diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..123730f7 Binary files /dev/null and b/.DS_Store differ diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..0909e1ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,10 @@ +## ๐Ÿš€ Issue Summary + + +#### ๐Ÿš— Implement TODO +- [ ] +- [ ] + + +#### ๐Ÿš“ Remarks + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..6e784b05 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +## ๐Ÿ“ PR Summary + + +#### ๐ŸŒต Working Branch + + +#### ๐ŸŒด Works +- [ ] +- [ ] + + +#### ๐ŸŒฑ Related Issue + diff --git a/.gitignore b/.gitignore index 9dc4f875..c1b73604 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ out/ ### application.yml ํŒŒ์ผ๋“ค์— ๋Œ€ํ•œ gitignore ์ฒ˜๋ฆฌ application-local.yml +application-prod.yml diff --git a/build.gradle b/build.gradle index 490b4bc7..bc8c7242 100644 --- a/build.gradle +++ b/build.gradle @@ -31,13 +31,32 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' + // swagger + implementation 'org.springdoc:springdoc-openapi-ui:1.6.12' + implementation 'org.springdoc:springdoc-openapi-security:1.6.12' + + // sentry + implementation 'io.sentry:sentry-spring-boot-starter:6.17.0' + implementation 'io.sentry:sentry-logback:6.17.0' + + // lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + // mapper + implementation 'org.mapstruct:mapstruct:1.4.2.Final' + annotationProcessor "org.mapstruct:mapstruct-processor:1.4.2.Final" + + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2' + // DB implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' + // postgres implementation group: 'org.postgresql', name: 'postgresql', version: '42.2.23' runtimeOnly 'org.postgresql:postgresql' @@ -56,6 +75,8 @@ dependencies { //aws-s3 implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.2.5.RELEASE' + // retry + implementation 'org.springframework.retry:spring-retry:1.2.5.RELEASE' // test testImplementation 'org.springframework.boot:spring-boot-starter-test' @@ -75,7 +96,7 @@ querydsl { sourceSets { main.java.srcDir querydslDir } -compileQuerydsl{ +compileQuerydsl { options.annotationProcessorPath = configurations.querydsl } diff --git a/src/main/java/org/sopt/app/application/auth/AuthUseCaseImpl.java b/src/main/java/org/sopt/app/application/auth/AuthUseCaseImpl.java deleted file mode 100644 index 2efbfe11..00000000 --- a/src/main/java/org/sopt/app/application/auth/AuthUseCaseImpl.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.sopt.app.application.auth; - -import lombok.RequiredArgsConstructor; -import org.sopt.app.common.exception.DbException; -import org.sopt.app.common.exception.ExistUserException; -import org.sopt.app.common.exception.UserNotFoundException; -import org.sopt.app.domain.entity.User; -import org.sopt.app.interfaces.postgres.UserRepository; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@Service -@RequiredArgsConstructor -public class AuthUseCaseImpl { - - private final UserRepository userRepository; - private final EncryptService encryptService; - - public void validate(String nickname, String email) { - if (nickname != null) validateNickname(nickname); - if (email != null) validateEmail(email); - } - - @Transactional - public void changePassword(String userId, String password) { - User user = userRepository.findUserById(Long.parseLong(userId)).orElseThrow(); - user.password = encryptService.encode(password); - } - - @Transactional - public void changeNickname(String userId, String nickname) { - User user = userRepository.findUserById(Long.parseLong(userId)).orElseThrow(); - user.nickname = nickname; - } - - private void validateEmail(String email) throws UserNotFoundException { - Optional user = userRepository.findUserByEmail(email); - if (user.isPresent()) throw new ExistUserException("์ด๋ฏธ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค."); - } - - private void validateNickname(String nickname) { - Optional user = userRepository.findUserByNickname(nickname); - if (user.isPresent()) throw new ExistUserException("์‚ฌ์šฉ ์ค‘์ธ ๋‹‰๋„ค์ž„์ž…๋‹ˆ๋‹ค."); - - } - - @Transactional - public void deleteUser(String userId) { - try { - userRepository.deleteById(Long.parseLong(userId)); - } catch (Exception e) { - throw new DbException(e); - } - } -} diff --git a/src/main/java/org/sopt/app/application/auth/CustomUserDetailService.java b/src/main/java/org/sopt/app/application/auth/CustomUserDetailService.java new file mode 100644 index 00000000..18f1a698 --- /dev/null +++ b/src/main/java/org/sopt/app/application/auth/CustomUserDetailService.java @@ -0,0 +1,21 @@ +package org.sopt.app.application.auth; + +import lombok.RequiredArgsConstructor; +import org.sopt.app.common.exception.NotFoundException; +import org.sopt.app.common.response.ErrorCode; +import org.sopt.app.interfaces.postgres.UserRepository; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class CustomUserDetailService implements UserDetailsService { + + private final UserRepository userRepository; + + public UserDetails loadUserByUsername(String username) throws NotFoundException { + return (UserDetails) userRepository.findUserById(Long.parseLong(username)) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND.getMessage())); + } +} diff --git a/src/main/java/org/sopt/app/application/auth/JwtTokenService.java b/src/main/java/org/sopt/app/application/auth/JwtTokenService.java new file mode 100644 index 00000000..8363dba2 --- /dev/null +++ b/src/main/java/org/sopt/app/application/auth/JwtTokenService.java @@ -0,0 +1,104 @@ +package org.sopt.app.application.auth; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Base64; +import java.util.Date; +import javax.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.joda.time.LocalDateTime; +import org.sopt.app.application.auth.PlaygroundAuthInfo.AppToken; +import org.sopt.app.application.user.UserInfo; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class JwtTokenService { + + private final CustomUserDetailService customUserDetailsService; + @Value("${jwt.secret}") + private String JWT_SECRET; + + private String encodeKey(String secretKey) { + return Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8)); + } + + private Key getSigningKey(String keyString) { + val secretKey = this.encodeKey(keyString); + return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); + } + + public PlaygroundAuthInfo.AppToken issueNewTokens(UserInfo.Id userId, + PlaygroundAuthInfo.PlaygroundMain playgroundMember) { + val accessToken = this.encodeJwtToken(userId, playgroundMember.getId()); + val refreshToken = this.encodeJwtRefreshToken(userId); + return AppToken.builder().accessToken(accessToken).refreshToken(refreshToken).build(); + } + + private String encodeJwtToken(UserInfo.Id userId, Long playgroundId) { + val now = LocalDateTime.now(); + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setIssuer("sopt-makers") + .setIssuedAt(now.toDate()) + .setSubject(userId.getId().toString()) + .setExpiration(now.plusDays(1).toDate()) + .claim("id", userId.getId()) + .claim("playgroundId", playgroundId) + .claim("roles", "USER") + .signWith(getSigningKey(JWT_SECRET), SignatureAlgorithm.HS256) + .compact(); + } + + private String encodeJwtRefreshToken(UserInfo.Id userId) { + val now = LocalDateTime.now(); + return Jwts.builder() + .setIssuedAt(now.toDate()) + .setSubject(userId.getId().toString()) + .setExpiration(now.plusDays(30).toDate()) + .claim("id", userId.getId()) + .claim("roles", "USER") + .signWith(getSigningKey(JWT_SECRET), SignatureAlgorithm.HS256) + .compact(); + } + + public UserInfo.Id getUserIdFromJwtToken(String token) { + val claims = Jwts.parser() + .setSigningKey(this.encodeKey(JWT_SECRET)) + .parseClaimsJws(token) + .getBody(); + return UserInfo.Id.builder().id(Long.parseLong(claims.getSubject())).build(); + } + + public Authentication getAuthentication(String token) { + val userDetails = customUserDetailsService.loadUserByUsername( + this.getUserIdFromJwtToken(token).getId().toString()); + return new UsernamePasswordAuthenticationToken(userDetails, "", + userDetails.getAuthorities()); + } + + + public Boolean validateToken(String token) { + try { + val claims = Jwts.parser() + .setSigningKey(this.encodeKey(JWT_SECRET)).parseClaimsJws(token); + + return !claims.getBody().getExpiration().before(new Date()); + } catch (Exception e) { + return false; + } + } + + public String getToken(HttpServletRequest request) { + return request.getHeader("Authorization"); + } +} diff --git a/src/main/java/org/sopt/app/application/auth/PlaygroundAuthInfo.java b/src/main/java/org/sopt/app/application/auth/PlaygroundAuthInfo.java new file mode 100644 index 00000000..99013ec8 --- /dev/null +++ b/src/main/java/org/sopt/app/application/auth/PlaygroundAuthInfo.java @@ -0,0 +1,109 @@ +package org.sopt.app.application.auth; + +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.sopt.app.domain.enums.UserStatus; + +public class PlaygroundAuthInfo { + + @Getter + @Builder + @ToString + public static class PlaygroundAccessToken { + + private String accessToken; + } + + @Getter + @Builder + @ToString + public static class AppToken { + + private String accessToken; + private String refreshToken; + } + + @Getter + @Setter + @ToString + public static class PlaygroundMain { + + private Long id; + private String name; + private Long generation; + private String profileImage; + private Boolean hasProfile; + private String accessToken; + private UserStatus status; + } + + @Getter + @Setter + @ToString + public static class PlaygroundProfile { + + private String name; + private String profileImage; + private List activities; + } + + @Getter + @Setter + @ToString + public static class PlaygroundActivity { + + private String cardinalInfo; + private List cardinalActivities; + } + + @Getter + @Setter + @ToString + public static class PlaygroundCardinalActivity { + + private Long id; + private Long generation; + private String team; + private String part; + private Boolean isProject; + } + + @Getter + @Builder + @ToString + public static class MainView { + + private MainViewUser user; + } + + @Getter + @Builder + @ToString + public static class MainViewUser { + + private UserStatus status; + private String name; + private String profileImage; + private List generationList; + } + + @Getter + @Setter + @ToString + public static class AccessToken { + + private String accessToken; + } + + @Getter + @Setter + @ToString + public static class RefreshedToken { + + private String accessToken; + private String errorCode; + } +} diff --git a/src/main/java/org/sopt/app/application/auth/PlaygroundAuthService.java b/src/main/java/org/sopt/app/application/auth/PlaygroundAuthService.java new file mode 100644 index 00000000..c585c6bb --- /dev/null +++ b/src/main/java/org/sopt/app/application/auth/PlaygroundAuthService.java @@ -0,0 +1,151 @@ +package org.sopt.app.application.auth; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.sopt.app.common.exception.BadRequestException; +import org.sopt.app.common.exception.UnauthorizedException; +import org.sopt.app.common.response.ErrorCode; +import org.sopt.app.domain.enums.UserStatus; +import org.sopt.app.presentation.auth.AuthRequest; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException.BadRequest; +import org.springframework.web.client.RestTemplate; + +@Service +@RequiredArgsConstructor +public class PlaygroundAuthService { + + private final RestTemplate restTemplate; + @Value("${makers.playground.server}") + private String baseURI; + @Value("${sopt.current.generation}") + private Long currentGeneration; + @Value("${makers.playground.x-api-key}") + private String apiKey; + @Value("${makers.playground.x-request-from}") + private String requestFrom; + + public PlaygroundAuthInfo.PlaygroundMain getPlaygroundInfo(String token) { + val member = this.getPlaygroundMember(token); + val playgroundProfile = this.getPlaygroundMemberProfile(token); + val generationList = playgroundProfile.getActivities().stream() + .map(activity -> activity.getCardinalActivities().get(0).getGeneration()).collect(Collectors.toList()); + member.setAccessToken(token); + member.setStatus(this.getStatus(generationList)); + return member; + } + + public AuthRequest.AccessTokenRequest getPlaygroundAccessToken(AuthRequest.CodeRequest codeRequest) { + val getTokenURL = baseURI + "/api/v1/idp/sso/auth"; + + val headers = new HttpHeaders(); + headers.add("content-type", "application/json;charset=UTF-8"); + + val entity = new HttpEntity(codeRequest, headers); + + try { + val response = restTemplate.exchange( + getTokenURL, + HttpMethod.POST, + entity, + AuthRequest.AccessTokenRequest.class + ); + return response.getBody(); + } catch (Exception e) { + throw new BadRequestException(ErrorCode.INVALID_PLAYGROUND_CODE.getMessage()); + } + } + + private PlaygroundAuthInfo.PlaygroundMain getPlaygroundMember(String accessToken) { + val getUserURL = baseURI + "/internal/api/v1/members/me"; + + val headers = new HttpHeaders(); + headers.add("content-type", "application/json;charset=UTF-8"); + headers.add("Authorization", accessToken); + + val entity = new HttpEntity(null, headers); + + try { + val response = restTemplate.exchange( + getUserURL, + HttpMethod.GET, + entity, + PlaygroundAuthInfo.PlaygroundMain.class + ); + return response.getBody(); + } catch (Exception e) { + throw new BadRequestException(ErrorCode.PLAYGROUND_USER_NOT_EXISTS.getMessage()); + } + } + + public PlaygroundAuthInfo.RefreshedToken refreshPlaygroundToken(AuthRequest.AccessTokenRequest tokenRequest) { + val getTokenURL = baseURI + "/internal/api/v1/idp/auth/token"; + + val headers = new HttpHeaders(); + headers.add("content-type", "application/json;charset=UTF-8"); + headers.add("x-api-key", apiKey); + headers.add("x-request-from", requestFrom); + + val entity = new HttpEntity(tokenRequest, headers); + + try { + val response = restTemplate.exchange( + getTokenURL, + HttpMethod.POST, + entity, + PlaygroundAuthInfo.RefreshedToken.class + ); + return response.getBody(); + } catch (BadRequest badRequest) { + throw new UnauthorizedException(ErrorCode.INVALID_PLAYGROUND_TOKEN.getMessage()); + } + } + + public PlaygroundAuthInfo.MainView getPlaygroundUserForMainView(String accessToken) { + val playgroundProfile = this.getPlaygroundMemberProfile(accessToken); + val generationList = playgroundProfile.getActivities().stream() + .map(activity -> activity.getCardinalActivities().get(0).getGeneration()).collect(Collectors.toList()); + Collections.sort(generationList, Collections.reverseOrder()); + val mainViewUser = PlaygroundAuthInfo.MainViewUser.builder() + .status(this.getStatus(generationList)) + .name(playgroundProfile.getName()) + .profileImage(playgroundProfile.getProfileImage()) + .generationList(generationList) + .build(); + return PlaygroundAuthInfo.MainView.builder().user(mainViewUser).build(); + } + + private UserStatus getStatus(List generationList) { + return generationList.contains(currentGeneration) ? UserStatus.ACTIVE : UserStatus.INACTIVE; + } + + private PlaygroundAuthInfo.PlaygroundProfile getPlaygroundMemberProfile(String accessToken) { + val getUserURL = baseURI + "/internal/api/v1/members/profile/me"; + + val headers = new HttpHeaders(); + headers.add("content-type", "application/json;charset=UTF-8"); + headers.add("Authorization", accessToken); + + val entity = new HttpEntity(null, headers); + + try { + val response = restTemplate.exchange( + getUserURL, + HttpMethod.GET, + entity, + PlaygroundAuthInfo.PlaygroundProfile.class + ); + return response.getBody(); + } catch (BadRequest e) { + throw new BadRequestException(ErrorCode.PLAYGROUND_PROFILE_NOT_EXISTS.getMessage()); + } + } + +} diff --git a/src/main/java/org/sopt/app/application/mission/MissionInfo.java b/src/main/java/org/sopt/app/application/mission/MissionInfo.java new file mode 100644 index 00000000..a30ec014 --- /dev/null +++ b/src/main/java/org/sopt/app/application/mission/MissionInfo.java @@ -0,0 +1,21 @@ +package org.sopt.app.application.mission; + +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +public class MissionInfo { + + @Getter + @Builder + @ToString + public static class Completeness { + + private Long id; + private String title; + private Integer level; + private List profileImage; + private Boolean isCompleted; + } +} diff --git a/src/main/java/org/sopt/app/application/mission/MissionService.java b/src/main/java/org/sopt/app/application/mission/MissionService.java index 76d397c0..4d3079ae 100644 --- a/src/main/java/org/sopt/app/application/mission/MissionService.java +++ b/src/main/java/org/sopt/app/application/mission/MissionService.java @@ -1,21 +1,19 @@ package org.sopt.app.application.mission; import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import lombok.val; import org.sopt.app.domain.entity.Mission; import org.sopt.app.domain.entity.Stamp; import org.sopt.app.interfaces.postgres.MissionRepository; import org.sopt.app.interfaces.postgres.StampRepository; -import org.sopt.app.presentation.mission.dto.MissionRequestDto; -import org.sopt.app.presentation.mission.dto.GetAllMissionResponseDto; +import org.sopt.app.presentation.mission.MissionRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Predicate; -import java.util.stream.Collectors; - @Service @RequiredArgsConstructor public class MissionService { @@ -23,21 +21,21 @@ public class MissionService { private final MissionRepository missionRepository; private final StampRepository stampRepository; - - public List findAllMission(String userId) { - List completedStamps = stampRepository.findAllByUserId(Long.parseLong(userId)); - List missions = missionRepository.findAll(); - return missions.stream() - .map(mission -> - GetAllMissionResponseDto.builder() - .id(mission.getId()) - .title(mission.getTitle()) - .level(mission.getLevel()) - .profileImage(mission.getProfileImage()) - .isCompleted(isCompletedMission(mission.getId(), completedStamps)) - .build()) - .sorted(Comparator.comparing(GetAllMissionResponseDto::getLevel)) - .collect(Collectors.toList()); + @Transactional(readOnly = true) + public List findAllMission(Long userId) { + val completedStampList = stampRepository.findAllByUserId(userId); + val missionList = missionRepository.findAll(); + return missionList.stream() + .map(mission -> MissionInfo.Completeness.builder() + .id(mission.getId()) + .title(mission.getTitle()) + .level(mission.getLevel()) + .profileImage(mission.getProfileImage()) + .isCompleted(isCompletedMission(mission.getId(), completedStampList)) + .build()) + .sorted(Comparator.comparing(MissionInfo.Completeness::getLevel) + .thenComparing(MissionInfo.Completeness::getTitle)) + .collect(Collectors.toList()); } private Boolean isCompletedMission(Long missionId, List completedStamps) { @@ -45,112 +43,43 @@ private Boolean isCompletedMission(Long missionId, List completedStamps) stamp -> stamp.getMissionId().equals(missionId)); } - // ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ - ์ด๋ฏธ์ง€ ๋ฏธํฌํ•จ @Transactional - public Mission uploadMission(MissionRequestDto missionRequestDto) { - Mission mission = this.convertMission(missionRequestDto); - return missionRepository.save(mission); - } - - // ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ - ์ด๋ฏธ์ง€ ํฌํ•จ - @Transactional - public Mission uploadMissionWithImg(MissionRequestDto missionRequestDto, List imgPaths) { - - List imgList = new ArrayList<>(imgPaths); - Mission mission = this.convertMissionImg(missionRequestDto, imgList); + public Mission uploadMission(MissionRequest.RegisterMissionRequest registerMissionRequest) { + val mission = Mission.builder() + .title(registerMissionRequest.getTitle()) + .level(registerMissionRequest.getLevel()) + .display(true) + .profileImage(List.of(registerMissionRequest.getImage())) + .build(); return missionRepository.save(mission); - } - //Mission ์™„๋ฃŒํ•œ ๋ฏธ์…˜๋งŒ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ - @Transactional - public List getCompleteMission(String userId) { - - //ํ—ค๋”์—์„œ ๋ฐ›์€ userId๋กœ Stamp ํ…Œ์ด๋ธ”์—์„œ ๋‹ฌ์„ฑํ•œ ๋ฏธ์…˜๋ฒˆํ˜ธ ๊ฐ€์ ธ์˜ค๊ธฐ - List stampList = stampRepository.findAllByUserId(Long.valueOf(userId)); - - //๋‹ฌ์„ฑํ•œ ๋ฏธ์…˜๋ฒˆํ˜ธ๋ฆฌ์ŠคํŠธ - List missionIdList = new ArrayList<>(); - for (Stamp stamp : stampList) { - missionIdList.add(stamp.getMissionId()); - } - - return missionRepository.findMissionIn(missionIdList); + @Transactional(readOnly = true) + public List getCompleteMission(Long userId) { + val stampList = stampRepository.findAllByUserId(userId); + val missionIdList = stampList.stream().map(Stamp::getMissionId).collect(Collectors.toList()); + return missionRepository.findMissionInOrderByLevelAndTitle(missionIdList); } -// private void getCompleteMission(List stampList, Mission mission) { -// -// mission. -// -// -//// coupon.updateUseCoupon( -//// couponUsedCommand -//// .getCpnNoList() -//// .stream() -//// .filter((CouponUsed command) -> -//// command.getCouponNumber().equals(coupon.getCouponId())) -//// .findAny().orElseThrow(IllegalArgumentException::new)); -// } - - /** - * ๋ฏธ์™„๋ฃŒ ๋ฏธ์…˜๋งŒ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ - */ - @Transactional - public List getIncompleteMission(String userId) { + @Transactional(readOnly = true) + public List getIncompleteMission(Long userId) { //์ „์ฒด ๋ฏธ์…˜ ์กฐํšŒํ•˜๊ธฐ - List missionList = missionRepository.findAll(); - List allMissionList = new ArrayList<>(); - for (Mission mission : missionList) { - allMissionList.add(mission.getId()); - } + val missionList = missionRepository.findAll(); + val missionIdList = missionList.stream().map(Mission::getId).collect(Collectors.toList()); //stamp์—์„œ userId๋กœ ๋‹ฌ์„ฑํ•œ mission ์กฐํšŒํ•˜๊ธฐ - List stampList = stampRepository.findAllByUserId(Long.valueOf(userId)); - List completeMissionIdList = new ArrayList<>(); - for (Stamp stamp : stampList) { - completeMissionIdList.add(stamp.getMissionId()); - } + val stampList = stampRepository.findAllByUserId(userId); + val completeMissionIdList = stampList.stream().map(Stamp::getMissionId).collect(Collectors.toList()); //๋‘ ๋ฆฌ์ŠคํŠธ ๋น„๊ตํ•ด์„œ ์ค‘๋ณต๊ฐ’ ์ œ๊ฑฐ - List inCompleteIdList = allMissionList.stream() + val inCompleteIdList = missionIdList.stream() .filter(all -> completeMissionIdList.stream().noneMatch(Predicate.isEqual(all))) .toList(); -// noneMatch: ์ค‘๋ณต X -// anyMatch: ์ค‘๋ณต O -// List oldList = Arrays.asList("1", "2", "3", "4"); -// List newList = Arrays.asList("3", "4", "5", "6"); -// -// List resultList1 = oldList.stream() -// .filter(old -> newList.stream().noneMatch(Predicate.isEqual(old))) -// .collect(Collectors.toList()); -// -// System.out.println(resultList1); // [1, 2] - - return missionRepository.findMissionIn(inCompleteIdList); - } - - - //Mission Entity ์–‘์‹์— ๋งž๊ฒŒ ๋ฐ์ดํ„ฐ ์„ธํŒ… - private Mission convertMissionImg(MissionRequestDto missionRequestDto, List imgList) { - return Mission.builder() - .title(missionRequestDto.getTitle()) - .level(missionRequestDto.getLevel()) - .display(true) - .profileImage(imgList) - .build(); + return missionRepository.findMissionInOrderByLevelAndTitle(inCompleteIdList); } - - private Mission convertMission(MissionRequestDto missionRequestDto) { - - return Mission.builder() - .title(missionRequestDto.getTitle()) - .level(missionRequestDto.getLevel()) - .display(true) - .build(); - } - } + diff --git a/src/main/java/org/sopt/app/application/operation/OperationInfo.java b/src/main/java/org/sopt/app/application/operation/OperationInfo.java new file mode 100644 index 00000000..b955f60c --- /dev/null +++ b/src/main/java/org/sopt/app/application/operation/OperationInfo.java @@ -0,0 +1,37 @@ +package org.sopt.app.application.operation; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +public class OperationInfo { + + @Getter + @Builder + @ToString + public static class MainView { + + private String announcement; + private Double attendanceScore; + } + + + @Getter + @Setter + @ToString + public static class ScoreResponse { + + private Boolean success; + private String message; + private Score data; + } + + @Getter + @Setter + @ToString + public static class Score { + + private Double score; + } +} diff --git a/src/main/java/org/sopt/app/application/operation/OperationService.java b/src/main/java/org/sopt/app/application/operation/OperationService.java new file mode 100644 index 00000000..404fe42f --- /dev/null +++ b/src/main/java/org/sopt/app/application/operation/OperationService.java @@ -0,0 +1,53 @@ +package org.sopt.app.application.operation; + +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.sopt.app.common.exception.BadRequestException; +import org.sopt.app.common.response.ErrorCode; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException.BadRequest; +import org.springframework.web.client.RestTemplate; + +@Service +@RequiredArgsConstructor +public class OperationService { + + private final RestTemplate restTemplate = new RestTemplate(); + + @Value("${makers.operation.server}") + private String baseURI; + + public OperationInfo.MainView getOperationForMainView(String accessToken) { + val scoreResponse = this.getAttendanceScore(accessToken); + return OperationInfo.MainView.builder() + .announcement("๊ณต์ง€๋‹ค!") + .attendanceScore(scoreResponse.getData().getScore()) + .build(); + } + + private OperationInfo.ScoreResponse getAttendanceScore(String accessToken) { + val getScoreURL = baseURI + "/api/v1/app/score"; + + val headers = new HttpHeaders(); + headers.add("content-type", "application/json;charset=UTF-8"); + headers.add("Authorization", accessToken); + + val entity = new HttpEntity(null, headers); + + try { + val response = restTemplate.exchange( + getScoreURL, + HttpMethod.GET, + entity, + OperationInfo.ScoreResponse.class + ); + return response.getBody(); + } catch (BadRequest e) { + throw new BadRequestException(ErrorCode.OPERATION_PROFILE_NOT_EXISTS.getMessage()); + } + } +} diff --git a/src/main/java/org/sopt/app/application/rank/RankInfo.java b/src/main/java/org/sopt/app/application/rank/RankInfo.java new file mode 100644 index 00000000..6284411e --- /dev/null +++ b/src/main/java/org/sopt/app/application/rank/RankInfo.java @@ -0,0 +1,31 @@ +package org.sopt.app.application.rank; + +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import org.sopt.app.domain.entity.Mission; + +public class RankInfo { + + @Getter + @Builder + @ToString + public static class Main { + + private Integer rank; + private String nickname; + private Long point; + private String profileMessage; + } + + @Getter + @Builder + @ToString + public static class Detail { + + private String nickname; + private String profileMessage; + private List userMissions; + } +} diff --git a/src/main/java/org/sopt/app/application/rank/RankService.java b/src/main/java/org/sopt/app/application/rank/RankService.java index 4f55ac66..8fc3b6a4 100644 --- a/src/main/java/org/sopt/app/application/rank/RankService.java +++ b/src/main/java/org/sopt/app/application/rank/RankService.java @@ -1,20 +1,16 @@ package org.sopt.app.application.rank; -import lombok.RequiredArgsConstructor; -import org.sopt.app.application.mission.MissionService; -import org.sopt.app.common.exception.ApiException; -import org.sopt.app.domain.entity.User; -import org.sopt.app.interfaces.postgres.UserRepository; -import org.sopt.app.presentation.rank.dto.FindAllRanksResponseDto; -import org.sopt.app.presentation.rank.dto.FindRankResponseDto; -import org.springframework.stereotype.Service; - import java.util.Comparator; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; - -import static org.sopt.app.common.ResponseCode.INVALID_RESPONSE; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.sopt.app.common.exception.BadRequestException; +import org.sopt.app.common.response.ErrorCode; +import org.sopt.app.domain.entity.User; +import org.sopt.app.interfaces.postgres.UserRepository; +import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor @@ -22,41 +18,22 @@ public class RankService { private final UserRepository userRepository; - private final MissionService missionService; - - //User ํ•œ๋งˆ๋”” ๋“ฑ๋กํ•˜๊ธฐ - public User updateProfileMessage(Long userId, String profileMessage) { - - User user = userRepository.findUserById(userId) - .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); - - user.updateProfileMessage(profileMessage); - return userRepository.save(user); - } - - public List findRanks(){ - List users = userRepository.findAll(); - AtomicInteger rankPoint = new AtomicInteger(1); - return users.stream().sorted( - Comparator.comparing(User::getPoints).reversed()) - .map(user -> FindAllRanksResponseDto.builder() - .rank(rankPoint.getAndIncrement()) - .userId(user.getId()) - .nickname(user.getNickname()) - .point(user.getPoints()) - .profileMessage(user.getProfileMessage()) - .build()) - .collect(Collectors.toList()); + public List findRanks() { + val userList = userRepository.findAll(); + val rankPoint = new AtomicInteger(1); + return userList.stream().sorted( + Comparator.comparing(User::getPoints).reversed()) + .map(user -> RankInfo.Main.builder() + .rank(rankPoint.getAndIncrement()) + .nickname(user.getNickname()) + .point(user.getPoints()) + .profileMessage(user.getProfileMessage()) + .build()) + .collect(Collectors.toList()); } - public FindRankResponseDto findRankById(Long userId) { - User user = userRepository.findUserById(userId).orElseThrow(() -> new ApiException(INVALID_RESPONSE)); - return FindRankResponseDto.builder() - .userId(userId) - .nickname(user.getNickname()) - .profileMessage(user.getProfileMessage()) - .userMissions(missionService.getCompleteMission(String.valueOf(userId))) - .build(); + public User findRankByNickname(String nickname) { + return userRepository.findUserByNickname(nickname) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND.getMessage())); } - } diff --git a/src/main/java/org/sopt/app/application/s3/S3Info.java b/src/main/java/org/sopt/app/application/s3/S3Info.java new file mode 100644 index 00000000..82b89d26 --- /dev/null +++ b/src/main/java/org/sopt/app/application/s3/S3Info.java @@ -0,0 +1,17 @@ +package org.sopt.app.application.s3; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +public class S3Info { + + @Getter + @Builder + @ToString + public static class PreSignedUrl { + + private String preSignedURL; + private String imageURL; + } +} diff --git a/src/main/java/org/sopt/app/application/s3/S3Service.java b/src/main/java/org/sopt/app/application/s3/S3Service.java new file mode 100644 index 00000000..b6c6562f --- /dev/null +++ b/src/main/java/org/sopt/app/application/s3/S3Service.java @@ -0,0 +1,128 @@ +package org.sopt.app.application.s3; + +import com.amazonaws.HttpMethod; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.joda.time.LocalDateTime; +import org.sopt.app.common.ResponseCode; +import org.sopt.app.common.exception.BadRequestException; +import org.sopt.app.common.exception.v1.ApiException; +import org.sopt.app.common.response.ErrorCode; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +@RequiredArgsConstructor +public class S3Service { + + private final ArrayList imageFileExtension = new ArrayList<>( + List.of(".jpg", ".jpeg", ".png", ".JPG", ".JPEG", ".PNG")); + + private final AmazonS3 s3Client; + + @Value("${cloud.aws.credentials.accesskey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretkey}") + private String secretKey; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.region.static}") + private String region; + + @Value("${cloud.aws.s3.uri}") + private String baseURI; + + + @PostConstruct + public AmazonS3Client amazonS3Client() { + val awsCredentials = new BasicAWSCredentials(accessKey, secretKey); //accessKey ์™€ secretKey๋ฅผ ์ด์šฉํ•˜์—ฌ ์ž๊ฒฉ์ฆ๋ช… ๊ฐ์ฒด๋ฅผ ์–ป๋Š”๋‹ค. + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) // region ์„ค์ • + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) // ์ž๊ฒฉ์ฆ๋ช…์„ ํ†ตํ•ด S3 Client๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. + .build(); + } + + + public List uploadDeprecated(List multipartFiles) { + if (multipartFiles == null || multipartFiles.get(0).isEmpty()) { + return new ArrayList<>(); + } + + return multipartFiles.stream().map(file -> { + val fileName = createFileName(file.getOriginalFilename()); + val objectMetadata = new ObjectMetadata(); + objectMetadata.setContentLength(file.getSize()); + objectMetadata.setContentType(file.getContentType()); + + try (InputStream inputStream = file.getInputStream()) { + s3Client.putObject( + new PutObjectRequest(bucket + "/mainpage/makers-app", fileName, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + return s3Client.getUrl(bucket + "/mainpage/makers-app", fileName).toString(); + } catch (IOException e) { + throw new ApiException(ResponseCode.INVALID_RESPONSE); + } + }).collect(Collectors.toList()); + } + + // ์ด๋ฏธ์ง€ํŒŒ์ผ๋ช… ์ค‘๋ณต ๋ฐฉ์ง€ + private String createFileName(String fileName) { + return UUID.randomUUID().toString().concat(getFileExtension(fileName)); + } + + // ํŒŒ์ผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + private String getFileExtension(String fileName) { + if (fileName.length() == 0) { + throw new BadRequestException("์œ ํšจํ•˜์ง€ ์•Š์€ ํŒŒ์ผ๋ช…์ž…๋‹ˆ๋‹ค."); + } + + val idxFileName = fileName.substring(fileName.lastIndexOf(".")); + if (!imageFileExtension.contains(idxFileName)) { + throw new BadRequestException("์œ ํšจํ•˜์ง€ ์•Š์€ ํ™•์žฅ์ž์ž…๋‹ˆ๋‹ค."); + } + return fileName.substring(fileName.lastIndexOf(".")); + } + + public S3Info.PreSignedUrl getPreSignedUrl(String folderName) { + val now = LocalDateTime.now(); + val folderURI = bucket + "/mainpage/makers-app-img/" + folderName; + val randomFileName = UUID.randomUUID(); + + URI uri; + try { + uri = this.s3Client.generatePresignedUrl(folderURI, + randomFileName.toString(), now.plusHours(1).toDate(), HttpMethod.PUT).toURI(); + } catch (NullPointerException | URISyntaxException e) { + throw new BadRequestException(ErrorCode.PRE_SIGNED_URI_ERROR.getMessage()); + } + + val preSignedURL = uri.toString(); + val imageURL = baseURI + folderName + "/" + randomFileName; + + return S3Info.PreSignedUrl.builder() + .preSignedURL(preSignedURL) + .imageURL(imageURL) + .build(); + } +} diff --git a/src/main/java/org/sopt/app/application/stamp/StampService.java b/src/main/java/org/sopt/app/application/stamp/StampService.java index 15235248..04705b99 100644 --- a/src/main/java/org/sopt/app/application/stamp/StampService.java +++ b/src/main/java/org/sopt/app/application/stamp/StampService.java @@ -1,20 +1,19 @@ package org.sopt.app.application.stamp; -import static org.sopt.app.common.ResponseCode.DUPLICATE_STAMP; -import static org.sopt.app.common.ResponseCode.INVALID_RESPONSE; - import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; -import org.sopt.app.common.exception.ApiException; -import org.sopt.app.domain.entity.Mission; +import lombok.val; +import org.sopt.app.common.exception.BadRequestException; +import org.sopt.app.common.response.ErrorCode; import org.sopt.app.domain.entity.Stamp; import org.sopt.app.domain.entity.User; import org.sopt.app.interfaces.postgres.MissionRepository; import org.sopt.app.interfaces.postgres.StampRepository; import org.sopt.app.interfaces.postgres.UserRepository; -import org.sopt.app.presentation.stamp.dto.StampRequestDto; +import org.sopt.app.presentation.stamp.StampRequest; +import org.sopt.app.presentation.stamp.StampRequest.RegisterStampRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -23,130 +22,143 @@ @RequiredArgsConstructor public class StampService { - private final StampRepository stampRepository; - - private final UserRepository userRepository; - - private final MissionRepository missionRepository; - - public Stamp findStamp(String userId, Long missionId) { - return stampRepository.findByUserIdAndMissionId(Long.valueOf(userId), missionId); - } - - @Transactional - public Stamp uploadStamp(StampRequestDto stampRequestDto, List imgPaths, String userId, - Long missionId) { - List imgList = new ArrayList<>(imgPaths); - Stamp stamp = this.convertStampImg(stampRequestDto, imgList, userId, missionId); - - //๋žญํฌ ๊ด€๋ จ ์ ์ˆ˜ ์ฒ˜๋ฆฌ - User user = userRepository.findUserById(Long.valueOf(userId)) - .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); + private final StampRepository stampRepository; - //๋ฏธ์…˜ ๋žญํฌ์ ์ˆ˜ ์•Œ์•„์˜ค๊ธฐ - Mission mission = missionRepository.findById(missionId) - .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); + private final UserRepository userRepository; - user.addPoints(mission.getLevel()); - userRepository.save(user); + private final MissionRepository missionRepository; - return stampRepository.save(stamp); - } - - //์‚ฌ์ง„ ์ˆ˜์ • ํ•  ๊ฒฝ์šฐ - @Transactional - public Stamp editStampWithImg(StampRequestDto stampRequestDto, List imgPaths, - String userId, - Long missionId) { - - Stamp stamp = stampRepository.findByUserIdAndMissionId(Long.valueOf(userId), missionId); - - if (StringUtils.hasText(stampRequestDto.getContents())) { - stamp.changeContents(stampRequestDto.getContents()); + @Transactional(readOnly = true) + public Stamp findStamp(Long userId, Long missionId) { + return stampRepository.findByUserIdAndMissionId(userId, missionId) + .orElseThrow(() -> new BadRequestException(ErrorCode.STAMP_NOT_FOUND.getMessage())); } - stamp.changeImages(imgPaths); - stamp.setUpdatedAt(LocalDateTime.now()); - return stampRepository.save(stamp); - } + @Transactional + public Stamp uploadStampDeprecated( + RegisterStampRequest stampRequest, + List imgPaths, + Long userId, + Long missionId) { + val imgList = new ArrayList<>(imgPaths); + val stamp = this.convertStampImgDeprecated(stampRequest, imgList, userId, missionId); + val user = userRepository.findUserById(Long.valueOf(userId)) + .orElseThrow(() -> new BadRequestException(ErrorCode.USER_NOT_FOUND.getMessage())); + val mission = missionRepository.findById(missionId) + .orElseThrow(() -> new BadRequestException(ErrorCode.MISSION_NOT_FOUND.getMessage())); - //์‚ฌ์ง„ ์ˆ˜์ • ์•ˆํ•  ๊ฒฝ์šฐ - @Transactional - public Stamp editStampContents(StampRequestDto stampRequestDto, String userId, - Long missionId) { + user.addPoints(mission.getLevel()); + userRepository.save(user); - Stamp stamp = stampRepository.findByUserIdAndMissionId(Long.valueOf(userId), missionId); - - if (StringUtils.hasText(stampRequestDto.getContents())) { - stamp.changeContents(stampRequestDto.getContents()); + return stampRepository.save(stamp); } - stamp.setUpdatedAt(LocalDateTime.now()); - return stampRepository.save(stamp); - } - - - //Stamp ์‚ญ์ œ by stampId - @Transactional - public void deleteByStampId(Long stampId) { - - Stamp stamp = stampRepository.findById(stampId) - .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); - - //๋žญํฌ ๊ด€๋ จ ์ ์ˆ˜ ์ฒ˜๋ฆฌ - User user = userRepository.findUserById(stamp.getUserId()) - .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); - - //๋ฏธ์…˜ ๋žญํฌ์ ์ˆ˜ ์•Œ์•„์˜ค๊ธฐ - Mission mission = missionRepository.findById(stamp.getMissionId()) - .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); - - user.minusPoints(mission.getLevel()); - userRepository.save(user); - - stampRepository.deleteById(stampId); - } + @Transactional + public Stamp uploadStamp( + RegisterStampRequest stampRequest, + User user, + Long missionId) { + + val mission = missionRepository.findById(missionId) + .orElseThrow(() -> new BadRequestException(ErrorCode.MISSION_NOT_FOUND.getMessage())); + val stamp = Stamp.builder() + .contents(stampRequest.getContents()) + .createdAt(LocalDateTime.now()) + .images(List.of(stampRequest.getImage())) + .missionId(missionId) + .userId(user.getId()) + .build(); + user.addPoints(mission.getLevel()); + userRepository.save(user); + + return stampRepository.save(stamp); + } + //์Šคํƒฌํ”„ ๋‚ด์šฉ ์ˆ˜์ • + @Transactional + public Stamp editStampContentsDeprecated( + StampRequest.EditStampRequest editStampRequest, + Long userId, + Long missionId) { + + val stamp = stampRepository.findByUserIdAndMissionId(userId, missionId) + .orElseThrow(() -> new BadRequestException(ErrorCode.STAMP_NOT_FOUND.getMessage())); + if (StringUtils.hasText(editStampRequest.getContents())) { + stamp.changeContents(editStampRequest.getContents()); + } + + stamp.setUpdatedAt(LocalDateTime.now()); + return stampRepository.save(stamp); + } - //์Šคํƒฌํ”„ ์ค‘๋ณต ๊ฒ€์‚ฌ์ฒดํฌ - @Transactional - public void checkDuplicateStamp(String userId, Long missionId){ - Stamp stamp = stampRepository.findByUserIdAndMissionId(Long.valueOf(userId), missionId); + @Transactional + public Stamp editStampContents( + StampRequest.EditStampRequest editStampRequest, + Long userId, + Long missionId) { + + val stamp = stampRepository.findByUserIdAndMissionId(userId, missionId) + .orElseThrow(() -> new BadRequestException(ErrorCode.STAMP_NOT_FOUND.getMessage())); + if (StringUtils.hasText(editStampRequest.getContents())) { + stamp.changeContents(editStampRequest.getContents()); + } + if (StringUtils.hasText(editStampRequest.getImage())) { + stamp.changeImages(List.of(editStampRequest.getImage())); + } + stamp.setUpdatedAt(LocalDateTime.now()); + return stampRepository.save(stamp); + } - if(stamp != null){ - throw new ApiException(DUPLICATE_STAMP); + //์Šคํƒฌํ”„ ์‚ฌ์ง„ ์ˆ˜์ • + @Transactional + public Stamp editStampImagesDeprecated(Stamp stamp, List imgPaths) { + stamp.changeImages(imgPaths); + return stampRepository.save(stamp); } - } + //Stamp ์‚ญ์ œ by stampId + @Transactional + public void deleteStampById(User user, Long stampId) { + val stamp = stampRepository.findById(stampId) + .orElseThrow(() -> new BadRequestException(ErrorCode.STAMP_NOT_FOUND.getMessage())); + val mission = missionRepository.findById(stamp.getMissionId()) + .orElseThrow(() -> new BadRequestException(ErrorCode.MISSION_NOT_FOUND.getMessage())); - //Stamp ์‚ญ์ œ All by UserId - @Transactional - public void deleteStampByUserId(Long userId){ + user.minusPoints(mission.getLevel()); + userRepository.save(user); + stampRepository.deleteById(stampId); + } - //์Šคํƒฌํ”„ ์ „๋ถ€์‚ญ์ œ - stampRepository.deleteAllByUserId(userId); - //ํ•ด๋‹น ์Šคํƒฌํ”„๋กœ ์–ป์—ˆ๋˜ ์ ์ˆ˜ ๋ชจ๋‘ ์ดˆ๊ธฐํ™” - User user = userRepository.findUserById(userId) - .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); - user.initializePoints(); - userRepository.save(user); + @Transactional(readOnly = true) + public void checkDuplicateStamp(Long userId, Long missionId) { + if (stampRepository.findByUserIdAndMissionId(userId, missionId).isPresent()) { + throw new BadRequestException(ErrorCode.DUPLICATE_STAMP.getMessage()); + } + } - } + @Transactional + public void deleteAllStamps(User user) { + stampRepository.deleteAllByUserId(user.getId()); + user.initializePoints(); + userRepository.save(user); + } - //Stamp Entity ์–‘์‹์— ๋งž๊ฒŒ ๋ฐ์ดํ„ฐ ์„ธํŒ… - private Stamp convertStampImg(StampRequestDto stampRequestDto, List imgList, - String userId, Long missionId) { - return Stamp.builder() - .contents(stampRequestDto.getContents()) - .createdAt(LocalDateTime.now()) - .images(imgList) - .missionId(missionId) - .userId(Long.valueOf(userId)) - .build(); - } + private Stamp convertStampImgDeprecated( + RegisterStampRequest stampRequest, + List imgList, + Long userId, + Long missionId) { + return Stamp.builder() + .contents(stampRequest.getContents()) + .createdAt(LocalDateTime.now()) + .images(imgList) + .missionId(missionId) + .userId(userId) + .build(); + } } diff --git a/src/main/java/org/sopt/app/application/user/UserInfo.java b/src/main/java/org/sopt/app/application/user/UserInfo.java new file mode 100644 index 00000000..f0ef8c93 --- /dev/null +++ b/src/main/java/org/sopt/app/application/user/UserInfo.java @@ -0,0 +1,40 @@ +package org.sopt.app.application.user; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +public class UserInfo { + + @Getter + @Builder + @ToString + public static class Id { + + private Long id; + } + + @Getter + @Builder + @ToString + public static class Token { + + private String accessToken; + } + + @Getter + @Builder + @ToString + public static class Nickname { + + private String nickname; + } + + @Getter + @Builder + @ToString + public static class ProfileMessage { + + private String profileMessage; + } +} diff --git a/src/main/java/org/sopt/app/application/user/UserService.java b/src/main/java/org/sopt/app/application/user/UserService.java new file mode 100644 index 00000000..1a066bac --- /dev/null +++ b/src/main/java/org/sopt/app/application/user/UserService.java @@ -0,0 +1,97 @@ +package org.sopt.app.application.user; + +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.sopt.app.application.auth.PlaygroundAuthInfo; +import org.sopt.app.common.exception.BadRequestException; +import org.sopt.app.common.exception.UnauthorizedException; +import org.sopt.app.common.response.ErrorCode; +import org.sopt.app.domain.entity.User; +import org.sopt.app.interfaces.postgres.UserRepository; +import org.sopt.app.presentation.auth.AuthRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + @Transactional + public UserInfo.Id loginWithUserPlaygroundId(PlaygroundAuthInfo.PlaygroundMain playgroundMemberResponse) { + val registeredUser = userRepository.findUserByPlaygroundId(playgroundMemberResponse.getId()); + + if (registeredUser.isPresent()) { + registeredUser.get() + .updatePlaygroundUserInfo(playgroundMemberResponse.getName(), + playgroundMemberResponse.getAccessToken()); + userRepository.save(registeredUser.get()); + + return UserInfo.Id.builder().id(registeredUser.get().getId()).build(); + } else { + val newUser = this.registerNewUser(playgroundMemberResponse.getName(), playgroundMemberResponse.getId(), + playgroundMemberResponse.getAccessToken()); + userRepository.save(newUser); + + return UserInfo.Id.builder().id(newUser.getId()).build(); + } + } + + private User registerNewUser(String username, Long playgroundId, String playgroundToken) { + val nickname = this.generateNickname(username); + return User.builder() + .username(username) + .nickname(nickname) + .email("") + .password("") + .osType(null) + .clientToken("") + .playgroundId(playgroundId) + .playgroundToken(playgroundToken) + .points(0L) + .build(); + } + + private String generateNickname(String username) { + return username + Math.round(Math.random() * 10000); + } + + @Transactional(readOnly = true) + public void checkUserNickname(String nickname) { + val nicknameUser = userRepository.findUserByNickname(nickname); + if (nicknameUser.isPresent()) { + throw new BadRequestException(ErrorCode.DUPLICATE_NICKNAME.getMessage()); + } + } + + @Transactional + public UserInfo.Nickname editNickname(User user, String nickname) { + user.editNickname(nickname); + userRepository.save(user); + return UserInfo.Nickname.builder().nickname(nickname).build(); + } + + @Transactional + public UserInfo.ProfileMessage editProfileMessage(User user, String profileMessage) { + user.updateProfileMessage(profileMessage); + userRepository.save(user); + return UserInfo.ProfileMessage.builder() + .profileMessage(user.getProfileMessage()) + .build(); + } + + @Transactional + public void deleteUser(User user) { + userRepository.delete(user); + } + + @Transactional(readOnly = true) + public AuthRequest.AccessTokenRequest getPlaygroundToken(UserInfo.Id userId) { + val user = userRepository.findUserById(userId.getId()) + .orElseThrow(() -> new UnauthorizedException(ErrorCode.INVALID_REFRESH_TOKEN.getMessage())); + val token = new AuthRequest.AccessTokenRequest(); + token.setAccessToken(user.getPlaygroundToken()); + return token; + } +} diff --git a/src/main/java/org/sopt/app/application/user/UserUseCase.java b/src/main/java/org/sopt/app/application/user/UserUseCase.java deleted file mode 100644 index ed541036..00000000 --- a/src/main/java/org/sopt/app/application/user/UserUseCase.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.sopt.app.application.user; - -import org.sopt.app.application.user.dto.LogInUserDto; -import org.sopt.app.application.user.dto.SignUpUserDto; -import org.sopt.app.domain.entity.User; - -public interface UserUseCase { - - Long signUp(SignUpUserDto userDto); - - User logIn(LogInUserDto userDto); -} diff --git a/src/main/java/org/sopt/app/application/user/UserUseCaseImpl.java b/src/main/java/org/sopt/app/application/user/UserUseCaseImpl.java deleted file mode 100644 index 117725e1..00000000 --- a/src/main/java/org/sopt/app/application/user/UserUseCaseImpl.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.sopt.app.application.user; - -import lombok.RequiredArgsConstructor; -import org.sopt.app.application.user.dto.LogInUserDto; -import org.sopt.app.application.user.dto.SignUpUserDto; -import org.sopt.app.application.auth.EncryptService; -import org.sopt.app.application.user.service.UserService; -import org.sopt.app.common.exception.UserNotFoundException; -import org.sopt.app.domain.entity.User; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class UserUseCaseImpl implements UserUseCase { - - private final UserService userService; - private final EncryptService encryptService; - - @Override - public Long signUp(SignUpUserDto userDto) { - String encodedPassword = encryptService.encode(userDto.getPassword()); - return userService.create(userDto, encodedPassword); - } - - @Override - public User logIn(LogInUserDto userDto) { - User user = userService.findUserByEmail(userDto.email()); - - boolean matches = encryptService.matches(userDto.password(), user.getPassword()); - if (!matches) throw new UserNotFoundException("์•„์ด๋””/๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”."); - - return user; - } -} diff --git a/src/main/java/org/sopt/app/application/user/service/UserService.java b/src/main/java/org/sopt/app/application/user/service/UserService.java deleted file mode 100644 index 225a452b..00000000 --- a/src/main/java/org/sopt/app/application/user/service/UserService.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.sopt.app.application.user.service; - -import lombok.RequiredArgsConstructor; -import org.sopt.app.application.user.dto.SignUpUserDto; -import org.sopt.app.common.exception.UserNotFoundException; -import org.sopt.app.domain.entity.User; -import org.sopt.app.interfaces.postgres.UserRepository; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class UserService { - - private final UserRepository userRepository; - - public Long create(SignUpUserDto userDto, String password) { - User user = userRepository.save(User.builder() - .nickname(userDto.getNickname()) - .email(userDto.getEmail()) - .password(password) - .osType(userDto.getOsType()) - .points(0L) - .clientToken(userDto.getClientToken()) - .build()); - - return user.getId(); - } - - public User findUserByEmail(String email) throws UserNotFoundException { - return userRepository.findUserByEmail(email).orElseThrow(() -> new UserNotFoundException("์•„์ด๋””/๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.")); - } -} diff --git a/src/main/java/org/sopt/app/common/ResponseCode.java b/src/main/java/org/sopt/app/common/ResponseCode.java index 73fdaca0..b5114019 100644 --- a/src/main/java/org/sopt/app/common/ResponseCode.java +++ b/src/main/java/org/sopt/app/common/ResponseCode.java @@ -1,20 +1,21 @@ package org.sopt.app.common; import com.google.common.collect.ImmutableMap; -import lombok.Getter; -import org.springframework.http.HttpStatus; - import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import lombok.Getter; +import org.springframework.http.HttpStatus; @Getter public enum ResponseCode { INVALID_RESPONSE("99", "99", "9999", "์š”์ฒญ์ด ์ฒ˜๋ฆฌ ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.", HttpStatus.INTERNAL_SERVER_ERROR), - SUCCESS("00","00", "0000", "์ •์ƒ ์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.OK), + SUCCESS("00", "00", "0000", "์ •์ƒ ์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", HttpStatus.OK), INVALID_REQUEST("00", "01", "0001", "์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), - DUPLICATE_STAMP("00", "02", "0002", "์ค‘๋ณต๋œ ์Šคํƒฌํ”„ ๋“ฑ๋ก ์š”์ฒญ์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST); + DUPLICATE_STAMP("00", "02", "0002", "์ค‘๋ณต๋œ ์Šคํƒฌํ”„ ๋“ฑ๋ก ์š”์ฒญ์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST), + ENTITY_NOT_FOUND("00", "03", "0003", "์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฆฌ์†Œ์Šค์ž…๋‹ˆ๋‹ค.", HttpStatus.NOT_FOUND), + UNAUTHORIZED("00", "03", "0004", "์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค.", HttpStatus.UNAUTHORIZED); private final String codeGroup; private final String code; @@ -36,11 +37,12 @@ public enum ResponseCode { } - public static ResponseCode getResponseCode(String responseCode){ - if(codes.get(responseCode)!=null) + public static ResponseCode getResponseCode(String responseCode) { + if (codes.get(responseCode) != null) { return codes.get(responseCode); - else + } else { return INVALID_RESPONSE; + } } // public String getUrlEncodingMessage(){ // return URLEncoder.encode(this.message, StandardCharsets.UTF_8); diff --git a/src/main/java/org/sopt/app/common/config/JwtAuthenticationFilter.java b/src/main/java/org/sopt/app/common/config/JwtAuthenticationFilter.java new file mode 100644 index 00000000..65faf112 --- /dev/null +++ b/src/main/java/org/sopt/app/common/config/JwtAuthenticationFilter.java @@ -0,0 +1,44 @@ +package org.sopt.app.common.config; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.sopt.app.application.auth.JwtTokenService; +import org.sopt.app.common.exception.NotFoundException; +import org.sopt.app.common.exception.UnauthorizedException; +import org.sopt.app.common.response.ErrorCode; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.GenericFilterBean; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends GenericFilterBean { + + private final JwtTokenService jwtTokenService; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + val token = jwtTokenService.getToken((HttpServletRequest) request); + if (token != null) { + if (jwtTokenService.validateToken(token)) { + try { + val authentication = jwtTokenService.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (NotFoundException e) { + throw new UnauthorizedException(ErrorCode.INVALID_ACCESS_TOKEN.getMessage()); + } + } else { + throw new UnauthorizedException(ErrorCode.INVALID_ACCESS_TOKEN.getMessage()); + } + } + chain.doFilter(request, response); + } + +} diff --git a/src/main/java/org/sopt/app/common/config/JwtExceptionFilter.java b/src/main/java/org/sopt/app/common/config/JwtExceptionFilter.java new file mode 100644 index 00000000..fd3a1b10 --- /dev/null +++ b/src/main/java/org/sopt/app/common/config/JwtExceptionFilter.java @@ -0,0 +1,37 @@ +package org.sopt.app.common.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import org.sopt.app.common.exception.UnauthorizedException; +import org.sopt.app.common.response.CommonResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +@AllArgsConstructor +public class JwtExceptionFilter extends OncePerRequestFilter { + + private ObjectMapper objectMapper; + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse, FilterChain chain) + throws ServletException, IOException { + try { + chain.doFilter(httpServletRequest, httpServletResponse); + } catch (UnauthorizedException e) { + httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); + httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); + httpServletResponse.setCharacterEncoding("UTF-8"); + objectMapper.writeValue(httpServletResponse.getWriter(), + CommonResponse.onFailure(e.getStatusCode(), e.getMessage())); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/common/config/OpenApiConfig.java b/src/main/java/org/sopt/app/common/config/OpenApiConfig.java new file mode 100644 index 00000000..7baa94b6 --- /dev/null +++ b/src/main/java/org/sopt/app/common/config/OpenApiConfig.java @@ -0,0 +1,27 @@ +package org.sopt.app.common.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile("!prod") +@OpenAPIDefinition( + info = @Info( + title = "SOPT APP Team API", + version = "v2", + description = "SOPT ๊ณต์‹ ์•ฑํŒ€ API์ž…๋‹ˆ๋‹ค." + ) +) +@SecurityScheme( + name = "Authorization", + type = SecuritySchemeType.APIKEY, + in = SecuritySchemeIn.HEADER +) +@Component +public class OpenApiConfig { + +} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/common/config/RestControllerAdviceHandler.java b/src/main/java/org/sopt/app/common/config/RestControllerAdviceHandler.java index 172d2590..8cfcbba3 100644 --- a/src/main/java/org/sopt/app/common/config/RestControllerAdviceHandler.java +++ b/src/main/java/org/sopt/app/common/config/RestControllerAdviceHandler.java @@ -1,8 +1,8 @@ package org.sopt.app.common.config; import org.sopt.app.common.constants.ErrorMessage; -import org.sopt.app.common.exception.ExistUserException; -import org.sopt.app.common.exception.UserNotFoundException; +import org.sopt.app.common.exception.v1.ExistUserException; +import org.sopt.app.common.exception.v1.UserNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.validation.BindException; import org.springframework.web.bind.annotation.ExceptionHandler; diff --git a/src/main/java/org/sopt/app/common/config/RestTemplateConfig.java b/src/main/java/org/sopt/app/common/config/RestTemplateConfig.java new file mode 100644 index 00000000..d33939c3 --- /dev/null +++ b/src/main/java/org/sopt/app/common/config/RestTemplateConfig.java @@ -0,0 +1,38 @@ +package org.sopt.app.common.config; + +import java.time.Duration; +import lombok.val; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.retry.policy.SimpleRetryPolicy; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.web.client.RestTemplate; + +@Configuration +class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(5)) + .additionalInterceptors(clientHttpRequestInterceptor()) + .build(); + } + + public ClientHttpRequestInterceptor clientHttpRequestInterceptor() { + return (request, body, execution) -> { + val retryTemplate = new RetryTemplate(); + val retryPolicy = new SimpleRetryPolicy(); + retryPolicy.setMaxAttempts(2); + retryTemplate.setRetryPolicy(retryPolicy); + try { + return retryTemplate.execute(context -> execution.execute(request, body)); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/common/config/SecurityConfiguration.java b/src/main/java/org/sopt/app/common/config/SecurityConfiguration.java deleted file mode 100644 index fb1bbd14..00000000 --- a/src/main/java/org/sopt/app/common/config/SecurityConfiguration.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.sopt.app.common.config; - -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.configuration.WebSecurityConfigurerAdapter; - -@Configuration -@EnableWebSecurity -public class SecurityConfiguration extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception{ - http - .cors().disable() - .csrf().disable() - .formLogin().disable() - .headers().frameOptions().disable(); - } -} diff --git a/src/main/java/org/sopt/app/common/config/WebSecurityConfig.java b/src/main/java/org/sopt/app/common/config/WebSecurityConfig.java new file mode 100644 index 00000000..e757e0a6 --- /dev/null +++ b/src/main/java/org/sopt/app/common/config/WebSecurityConfig.java @@ -0,0 +1,80 @@ +package org.sopt.app.common.config; + +import java.util.Arrays; +import lombok.RequiredArgsConstructor; +import org.sopt.app.application.auth.JwtTokenService; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.firewall.DefaultHttpFirewall; +import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@RequiredArgsConstructor +@EnableWebSecurity +public class WebSecurityConfig { + + private static final String[] SwaggerPatterns = { + "/swagger-resources/**", + "/swagger-ui/**", + "/v3/api-docs/**", + "/webjars/**" + }; + private final JwtTokenService jwtTokenService; + private final JwtExceptionFilter jwtExceptionFilter; + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + jwtAuthenticationFilter = new JwtAuthenticationFilter(jwtTokenService); + + http.httpBasic().disable() + .cors().configurationSource(corsConfigurationSource()) + .and() + .csrf().disable() + .authorizeRequests() + .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() + .antMatchers(SwaggerPatterns).permitAll() + .antMatchers("/api/v1/**").permitAll() + .antMatchers("/api/v2/health/**").permitAll() + .antMatchers("/api/v2/auth/**").permitAll() + .antMatchers("/api/v2/config/**").permitAll() + .antMatchers("/api/v2/firebase/**").permitAll() + .anyRequest().authenticated() + .and() + .addFilterBefore(jwtAuthenticationFilter, + UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class); + + return http.build(); + } + + @Bean + protected CorsConfigurationSource corsConfigurationSource() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", getDefaultCorsConfiguration()); + + return source; + } + + private CorsConfiguration getDefaultCorsConfiguration() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("*")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowedMethods(Arrays.asList("*")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + + return configuration; + } + + @Bean + public HttpFirewall defaultHttpFirewall() { + return new DefaultHttpFirewall(); + } +} diff --git a/src/main/java/org/sopt/app/common/exception/ApiExceptionHandler.java b/src/main/java/org/sopt/app/common/exception/ApiExceptionHandler.java deleted file mode 100644 index 47ec5e39..00000000 --- a/src/main/java/org/sopt/app/common/exception/ApiExceptionHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.sopt.app.common.exception; - -import static org.sopt.app.common.constants.Constants.RESULT_CODE; -import static org.sopt.app.common.constants.Constants.RESULT_MESSAGE; - -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -@RestControllerAdvice -public class ApiExceptionHandler { - - @ExceptionHandler({ApiException.class}) - protected void handle(ApiException apiException, HttpServletRequest request, - HttpServletResponse response) { - - final String encodingResultMessage = URLEncoder - .encode(apiException.getMessage(), StandardCharsets.UTF_8); - - response.setHeader(RESULT_CODE, apiException.getResultCode()); - response.setHeader(RESULT_MESSAGE, encodingResultMessage); - response.setStatus(apiException.getHttpStatus().value()); - } - -} diff --git a/src/main/java/org/sopt/app/common/exception/BadRequestException.java b/src/main/java/org/sopt/app/common/exception/BadRequestException.java new file mode 100644 index 00000000..0aed396e --- /dev/null +++ b/src/main/java/org/sopt/app/common/exception/BadRequestException.java @@ -0,0 +1,15 @@ +package org.sopt.app.common.exception; + +import org.springframework.http.HttpStatus; + +public class BadRequestException extends BaseException { + + public BadRequestException() { + super(HttpStatus.BAD_REQUEST); + } + + public BadRequestException(String message) { + super(HttpStatus.BAD_REQUEST, message); + } + +} diff --git a/src/main/java/org/sopt/app/common/exception/BaseException.java b/src/main/java/org/sopt/app/common/exception/BaseException.java new file mode 100644 index 00000000..0f3c7f7b --- /dev/null +++ b/src/main/java/org/sopt/app/common/exception/BaseException.java @@ -0,0 +1,25 @@ +package org.sopt.app.common.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class BaseException extends RuntimeException { + + HttpStatus statusCode; + String responseMessage; + + public BaseException() { + } + + public BaseException(HttpStatus statusCode) { + super(); + this.statusCode = statusCode; + } + + public BaseException(HttpStatus statusCode, String responseMessage) { + super(responseMessage); + this.statusCode = statusCode; + this.responseMessage = responseMessage; + } +} diff --git a/src/main/java/org/sopt/app/common/exception/ForbiddenException.java b/src/main/java/org/sopt/app/common/exception/ForbiddenException.java new file mode 100644 index 00000000..0a9e5636 --- /dev/null +++ b/src/main/java/org/sopt/app/common/exception/ForbiddenException.java @@ -0,0 +1,14 @@ +package org.sopt.app.common.exception; + +import org.springframework.http.HttpStatus; + +public class ForbiddenException extends BaseException { + + public ForbiddenException() { + super(HttpStatus.FORBIDDEN); + } + + public ForbiddenException(String message) { + super(HttpStatus.FORBIDDEN, message); + } +} diff --git a/src/main/java/org/sopt/app/common/exception/NotFoundException.java b/src/main/java/org/sopt/app/common/exception/NotFoundException.java new file mode 100644 index 00000000..e2651b48 --- /dev/null +++ b/src/main/java/org/sopt/app/common/exception/NotFoundException.java @@ -0,0 +1,15 @@ +package org.sopt.app.common.exception; + +import org.springframework.http.HttpStatus; + +public class NotFoundException extends BaseException { + + public NotFoundException() { + super(HttpStatus.NOT_FOUND); + } + + public NotFoundException(String message) { + super(HttpStatus.NOT_FOUND, message); + } + +} diff --git a/src/main/java/org/sopt/app/common/exception/UnauthorizedException.java b/src/main/java/org/sopt/app/common/exception/UnauthorizedException.java new file mode 100644 index 00000000..f88e2fa9 --- /dev/null +++ b/src/main/java/org/sopt/app/common/exception/UnauthorizedException.java @@ -0,0 +1,14 @@ +package org.sopt.app.common.exception; + +import org.springframework.http.HttpStatus; + +public class UnauthorizedException extends BaseException { + + public UnauthorizedException() { + super(HttpStatus.UNAUTHORIZED); + } + + public UnauthorizedException(String message) { + super(HttpStatus.UNAUTHORIZED, message); + } +} diff --git a/src/main/java/org/sopt/app/common/exception/UserNotFoundException.java b/src/main/java/org/sopt/app/common/exception/UserNotFoundException.java deleted file mode 100644 index 408ef7f9..00000000 --- a/src/main/java/org/sopt/app/common/exception/UserNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.sopt.app.common.exception; - -public class UserNotFoundException extends RuntimeException{ - public UserNotFoundException(String message) { - super(message); - } -} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/common/exception/ApiException.java b/src/main/java/org/sopt/app/common/exception/v1/ApiException.java similarity index 98% rename from src/main/java/org/sopt/app/common/exception/ApiException.java rename to src/main/java/org/sopt/app/common/exception/v1/ApiException.java index 0747cb16..3315dcf9 100644 --- a/src/main/java/org/sopt/app/common/exception/ApiException.java +++ b/src/main/java/org/sopt/app/common/exception/v1/ApiException.java @@ -1,4 +1,4 @@ -package org.sopt.app.common.exception; +package org.sopt.app.common.exception.v1; import lombok.Getter; import org.sopt.app.common.ResponseCode; diff --git a/src/main/java/org/sopt/app/common/exception/v1/ApiExceptionHandler.java b/src/main/java/org/sopt/app/common/exception/v1/ApiExceptionHandler.java new file mode 100644 index 00000000..f27b115c --- /dev/null +++ b/src/main/java/org/sopt/app/common/exception/v1/ApiExceptionHandler.java @@ -0,0 +1,28 @@ +package org.sopt.app.common.exception.v1; + +import static org.sopt.app.common.constants.Constants.RESULT_CODE; +import static org.sopt.app.common.constants.Constants.RESULT_MESSAGE; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class ApiExceptionHandler { + + @ExceptionHandler({ApiException.class}) + protected void handle(ApiException apiException, HttpServletRequest request, + HttpServletResponse response) { + + final String encodingResultMessage = URLEncoder + .encode(apiException.getMessage(), StandardCharsets.UTF_8); + + response.setHeader(RESULT_CODE, apiException.getResultCode()); + response.setHeader(RESULT_MESSAGE, encodingResultMessage); + response.setStatus(apiException.getHttpStatus().value()); + } + +} diff --git a/src/main/java/org/sopt/app/common/exception/DbException.java b/src/main/java/org/sopt/app/common/exception/v1/DbException.java similarity index 75% rename from src/main/java/org/sopt/app/common/exception/DbException.java rename to src/main/java/org/sopt/app/common/exception/v1/DbException.java index 9f1ec5f4..4a612146 100644 --- a/src/main/java/org/sopt/app/common/exception/DbException.java +++ b/src/main/java/org/sopt/app/common/exception/v1/DbException.java @@ -1,4 +1,4 @@ -package org.sopt.app.common.exception; +package org.sopt.app.common.exception.v1; public class DbException extends RuntimeException { diff --git a/src/main/java/org/sopt/app/common/exception/v1/EntityNotFoundException.java b/src/main/java/org/sopt/app/common/exception/v1/EntityNotFoundException.java new file mode 100644 index 00000000..612eb986 --- /dev/null +++ b/src/main/java/org/sopt/app/common/exception/v1/EntityNotFoundException.java @@ -0,0 +1,20 @@ +package org.sopt.app.common.exception.v1; + + +import lombok.Getter; +import org.sopt.app.common.ResponseCode; +import org.springframework.http.HttpStatus; + +public class EntityNotFoundException extends RuntimeException { + + @Getter + private final String resultCode; + @Getter + private final HttpStatus httpStatus; + + public EntityNotFoundException(ResponseCode responseCode) { + super("[" + responseCode.getResponseCode() + "] " + responseCode.getMessage()); + this.resultCode = responseCode.getResponseCode(); + this.httpStatus = responseCode.getHttpStatus(); + } +} diff --git a/src/main/java/org/sopt/app/common/exception/ExistUserException.java b/src/main/java/org/sopt/app/common/exception/v1/ExistUserException.java similarity index 76% rename from src/main/java/org/sopt/app/common/exception/ExistUserException.java rename to src/main/java/org/sopt/app/common/exception/v1/ExistUserException.java index 8b5e0b7b..65034f3b 100644 --- a/src/main/java/org/sopt/app/common/exception/ExistUserException.java +++ b/src/main/java/org/sopt/app/common/exception/v1/ExistUserException.java @@ -1,6 +1,7 @@ -package org.sopt.app.common.exception; +package org.sopt.app.common.exception.v1; public class ExistUserException extends RuntimeException { + public ExistUserException(String message) { super(message); } diff --git a/src/main/java/org/sopt/app/common/exception/v1/UserNotFoundException.java b/src/main/java/org/sopt/app/common/exception/v1/UserNotFoundException.java new file mode 100644 index 00000000..a247a556 --- /dev/null +++ b/src/main/java/org/sopt/app/common/exception/v1/UserNotFoundException.java @@ -0,0 +1,8 @@ +package org.sopt.app.common.exception.v1; + +public class UserNotFoundException extends RuntimeException { + + public UserNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/common/response/CommonControllerAdvice.java b/src/main/java/org/sopt/app/common/response/CommonControllerAdvice.java new file mode 100644 index 00000000..b3372df7 --- /dev/null +++ b/src/main/java/org/sopt/app/common/response/CommonControllerAdvice.java @@ -0,0 +1,24 @@ +package org.sopt.app.common.response; + +import org.sopt.app.common.exception.BaseException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class CommonControllerAdvice { + + @ExceptionHandler(value = BaseException.class) + public ResponseEntity onKnownException(BaseException baseException) { + return new ResponseEntity<>(CommonResponse.onFailure(baseException.getStatusCode(), + baseException.getResponseMessage()), null, baseException.getStatusCode()); + } + + @ExceptionHandler(value = Exception.class) + public ResponseEntity onException(Exception exception) { + exception.printStackTrace(); + return new ResponseEntity<>(CommonResponse.onFailure(HttpStatus.INTERNAL_SERVER_ERROR, + "์„œ๋ฒ„ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."), null, HttpStatus.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/common/response/CommonResponse.java b/src/main/java/org/sopt/app/common/response/CommonResponse.java new file mode 100644 index 00000000..ab69aad4 --- /dev/null +++ b/src/main/java/org/sopt/app/common/response/CommonResponse.java @@ -0,0 +1,21 @@ +package org.sopt.app.common.response; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@EqualsAndHashCode +@NoArgsConstructor +@AllArgsConstructor +public class CommonResponse { + + HttpStatus statusCode; + String responseMessage; + + public static CommonResponse onFailure(HttpStatus statusCode, String responseMessage) { + return new CommonResponse<>(statusCode, responseMessage); + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/common/response/ErrorCode.java b/src/main/java/org/sopt/app/common/response/ErrorCode.java new file mode 100644 index 00000000..af50c8b3 --- /dev/null +++ b/src/main/java/org/sopt/app/common/response/ErrorCode.java @@ -0,0 +1,42 @@ +package org.sopt.app.common.response; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + + + // COMMON + INVALID_PARAMETER("์ž˜๋ชป๋œ ํŒŒ๋ผ๋ฏธํ„ฐ ์ž…๋‹ˆ๋‹ค."), + + // AUTH + INVALID_ACCESS_TOKEN("์œ ํšจํ•˜์ง€ ์•Š์€ ์•ฑ ์–ด์„ธ์Šค ํ† ํฐ์ž…๋‹ˆ๋‹ค."), + INVALID_REFRESH_TOKEN("์œ ํšจํ•˜์ง€ ์•Š์€ ์•ฑ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ž…๋‹ˆ๋‹ค."), + INVALID_PLAYGROUND_TOKEN("์œ ํšจํ•˜์ง€ ์•Š์€ ํ”Œ๋ ˆ์ด๊ทธ๋ผ์šด๋“œ ํ† ํฐ์ž…๋‹ˆ๋‹ค."), + INVALID_PLAYGROUND_CODE("์œ ํšจํ•˜์ง€ ์•Š์€ ํ”Œ๋ ˆ์ด๊ทธ๋ผ์šด๋“œ OAuth ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค."), + + // PLAYGROUND + PLAYGROUND_USER_NOT_EXISTS("ํ”Œ๋ ˆ์ด๊ทธ๋ผ์šด๋“œ ์œ ์ € ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + PLAYGROUND_PROFILE_NOT_EXISTS("ํ”Œ๋ ˆ์ด๊ทธ๋ผ์šด๋“œ ํ”„๋กœํ•„์„ ๋“ฑ๋กํ•˜์ง€ ์•Š์€ ์œ ์ €์ž…๋‹ˆ๋‹ค."), + + // OPERATION + OPERATION_PROFILE_NOT_EXISTS("์šด์˜ ์„œ๋น„์Šค์— ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์›์ž…๋‹ˆ๋‹ค."), + + // USER + USER_NOT_FOUND("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €์ž…๋‹ˆ๋‹ค."), + DUPLICATE_NICKNAME("์‚ฌ์šฉ ์ค‘์ธ ๋‹‰๋„ค์ž„์ž…๋‹ˆ๋‹ค."), + + // MISSION + MISSION_NOT_FOUND("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฏธ์…˜์ž…๋‹ˆ๋‹ค."), + + // STAMP + STAMP_NOT_FOUND("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์Šคํƒฌํ”„์ž…๋‹ˆ๋‹ค."), + DUPLICATE_STAMP("์ด๋ฏธ ํ•ด๋‹น ๋ฏธ์…˜์— ๋Œ€ํ•œ ์Šคํƒฌํ”„๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค."), + + // S3 + PRE_SIGNED_URI_ERROR("URL์„ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + + private final String message; +} diff --git a/src/main/java/org/sopt/app/common/s3/S3Service.java b/src/main/java/org/sopt/app/common/s3/S3Service.java deleted file mode 100644 index a6f80cee..00000000 --- a/src/main/java/org/sopt/app/common/s3/S3Service.java +++ /dev/null @@ -1,119 +0,0 @@ -package org.sopt.app.common.s3; - -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import com.amazonaws.services.s3.model.CannedAccessControlList; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PutObjectRequest; -import lombok.RequiredArgsConstructor; -import org.sopt.app.common.ResponseCode; -import org.sopt.app.common.exception.ApiException; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import javax.annotation.PostConstruct; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@Service -@RequiredArgsConstructor -public class S3Service { - - private final AmazonS3 s3Client; - - @Value("${cloud.aws.credentials.accesskey}") - private String accessKey; - - @Value("${cloud.aws.credentials.secretkey}") - private String secretKey; - - @Value("${cloud.aws.s3.bucket}") - private String bucket; - - @Value("${cloud.aws.region.static}") - private String region; - - - @PostConstruct - public AmazonS3Client amazonS3Client() { - BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey); //accessKey ์™€ secretKey๋ฅผ ์ด์šฉํ•˜์—ฌ ์ž๊ฒฉ์ฆ๋ช… ๊ฐ์ฒด๋ฅผ ์–ป๋Š”๋‹ค. - return (AmazonS3Client) AmazonS3ClientBuilder.standard() - .withRegion(region) // region ์„ค์ • - .withCredentials(new AWSStaticCredentialsProvider(awsCreds)) // ์ž๊ฒฉ์ฆ๋ช…์„ ํ†ตํ•ด S3 Client๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. - .build(); - } - - - public List upload(List multipartFile) { - List imgUrlList = new ArrayList<>(); - - // forEach ๊ตฌ๋ฌธ์„ ํ†ตํ•ด multipartFile๋กœ ๋„˜์–ด์˜จ ํŒŒ์ผ๋“ค ํ•˜๋‚˜์”ฉ fileNameList์— ์ถ”๊ฐ€ - for (MultipartFile file : multipartFile) { - String fileName = createFileName(file.getOriginalFilename()); - ObjectMetadata objectMetadata = new ObjectMetadata(); - objectMetadata.setContentLength(file.getSize()); - objectMetadata.setContentType(file.getContentType()); - - try(InputStream inputStream = file.getInputStream()) { - s3Client.putObject(new PutObjectRequest(bucket+"/mainpage/makers-app", fileName, inputStream, objectMetadata) - .withCannedAcl(CannedAccessControlList.PublicRead)); - imgUrlList.add(s3Client.getUrl(bucket+"/mainpage/makers-app", fileName).toString()); - } catch(IOException e) { - throw new ApiException(ResponseCode.INVALID_RESPONSE); - } - } - return imgUrlList; - } - - // ์ด๋ฏธ์ง€ํŒŒ์ผ๋ช… ์ค‘๋ณต ๋ฐฉ์ง€ - private String createFileName(String fileName) { - return UUID.randomUUID().toString().concat(getFileExtension(fileName)); - } - - // ํŒŒ์ผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ - private String getFileExtension(String fileName) { - if (fileName.length() == 0) { - throw new ApiException(ResponseCode.INVALID_RESPONSE); - } - ArrayList fileValidate = new ArrayList<>(); - fileValidate.add(".jpg"); - fileValidate.add(".jpeg"); - fileValidate.add(".png"); - fileValidate.add(".JPG"); - fileValidate.add(".JPEG"); - fileValidate.add(".PNG"); - String idxFileName = fileName.substring(fileName.lastIndexOf(".")); - if (!fileValidate.contains(idxFileName)) { - throw new ApiException(ResponseCode.INVALID_RESPONSE); - } - return fileName.substring(fileName.lastIndexOf(".")); - } - - - -// @PostConstruct -// public void setS3Client() { -// AWSCredentials credentials = new BasicAWSCredentials(this.accessKey, this.secretKey); //accessKey ์™€ secretKey๋ฅผ ์ด์šฉํ•˜์—ฌ ์ž๊ฒฉ์ฆ๋ช… ๊ฐ์ฒด๋ฅผ ์–ป๋Š”๋‹ค. -// s3Client = AmazonS3ClientBuilder.standard() -// .withCredentials(new AWSStaticCredentialsProvider(credentials)) // ์ž๊ฒฉ์ฆ๋ช…์„ ํ†ตํ•ด S3 Client๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. -// .withRegion(this.region) // region ์„ค์ • -// .build(); -// } -// -// public String upload(MultipartFile file) throws IOException { -// String fileName = file.getOriginalFilename(); -// -// // ์—…๋กœ๋“œ ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” ํ•จ์ˆ˜ -// s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null) -// .withCannedAcl(CannedAccessControlList.PublicRead)); //์™ธ๋ถ€์— ๊ณต๊ฐœํ•ด์•ผ ํ•˜๋ฏ€๋กœ Public read ๊ถŒํ•œ์„ ์ค€๋‹ค. -// return s3Client.getUrl(bucket, fileName).toString(); -// } - -} diff --git a/src/main/java/org/sopt/app/domain/entity/Mission.java b/src/main/java/org/sopt/app/domain/entity/Mission.java index f4655871..c6ccfe00 100644 --- a/src/main/java/org/sopt/app/domain/entity/Mission.java +++ b/src/main/java/org/sopt/app/domain/entity/Mission.java @@ -2,46 +2,52 @@ import com.vladmihalcea.hibernate.type.array.ListArrayType; +import java.util.List; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; -import javax.persistence.*; -import java.util.List; - @Entity @Table(name = "mission", schema = "app_dev") @TypeDef( - name = "list-array", - typeClass = ListArrayType.class + name = "list-array", + typeClass = ListArrayType.class ) @Getter +@Setter @Builder @NoArgsConstructor @AllArgsConstructor public class Mission { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "mission_id") - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "mission_id") + private Long id; - @Column - private String title; + @Column + private String title; - @Column(name = "level") - private Integer level; + @Column(name = "level") + private Integer level; - @Column - private boolean display; + @Column + private boolean display; - @Type(type = "list-array") - @Column( - name = "profile_image", - columnDefinition = "text[]" - ) - private List profileImage; + @Type(type = "list-array") + @Column( + name = "profile_image", + columnDefinition = "text[]" + ) + private List profileImage; } diff --git a/src/main/java/org/sopt/app/domain/entity/User.java b/src/main/java/org/sopt/app/domain/entity/User.java index 5fed44ee..b51ba658 100644 --- a/src/main/java/org/sopt/app/domain/entity/User.java +++ b/src/main/java/org/sopt/app/domain/entity/User.java @@ -1,12 +1,21 @@ package org.sopt.app.domain.entity; +import java.util.Collection; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.sopt.app.domain.enums.OsType; - -import javax.persistence.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; @Entity @Table(name = "app_users", schema = "app_dev") @@ -14,22 +23,20 @@ @Builder @AllArgsConstructor @NoArgsConstructor -public class User extends BaseEntity { +public class User extends BaseEntity implements UserDetails { + @Column + public String username; + @Column(nullable = false, unique = true) + public String nickname; + @Column + public String password; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") private Long id; - - @Column(nullable = false) - public String nickname; - - @Column(nullable = false) + @Column private String email; - - @Column(nullable = false) - public String password; - @Column private String clientToken; @@ -43,13 +50,23 @@ public class User extends BaseEntity { @Enumerated(EnumType.STRING) private OsType osType; + @Column(name = "playground_id") + private Long playgroundId; + + @Column + private String playgroundToken; + @Builder - public User(String email, String nickname, String clientToken, OsType osType, String password) { + public User(String email, String username, String nickname, String clientToken, OsType osType, String password, + Long playgroundId) { + this.username = username; this.nickname = nickname; this.email = email; this.password = password; this.osType = osType; this.clientToken = clientToken; + this.playgroundId = playgroundId; + this.points = 0L; } public void createProfileMessage(User user, String profileMessage) { @@ -85,4 +102,38 @@ public void minusPoints(Integer level) { public void initializePoints() { this.points = 0L; } + + public void updatePlaygroundUserInfo(String username, String playgroundToken) { + this.username = username; + this.playgroundToken = playgroundToken; + } + + public void editNickname(String nickname) { + this.nickname = nickname; + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public boolean isAccountNonExpired() { + return false; + } + + @Override + public boolean isAccountNonLocked() { + return false; + } + + @Override + public boolean isCredentialsNonExpired() { + return false; + } + + @Override + public boolean isEnabled() { + return false; + } } diff --git a/src/main/java/org/sopt/app/domain/enums/UserStatus.java b/src/main/java/org/sopt/app/domain/enums/UserStatus.java new file mode 100644 index 00000000..1e85f5c6 --- /dev/null +++ b/src/main/java/org/sopt/app/domain/enums/UserStatus.java @@ -0,0 +1,7 @@ +package org.sopt.app.domain.enums; + +public enum UserStatus { + ACTIVE, + INACTIVE, + UNAUTHENTICATED +} diff --git a/src/main/java/org/sopt/app/interfaces/postgres/MissionRepository.java b/src/main/java/org/sopt/app/interfaces/postgres/MissionRepository.java index f3b9ec26..8dedbf42 100644 --- a/src/main/java/org/sopt/app/interfaces/postgres/MissionRepository.java +++ b/src/main/java/org/sopt/app/interfaces/postgres/MissionRepository.java @@ -8,7 +8,7 @@ public interface MissionRepository extends JpaRepository { - @Query("select m from Mission m where m.id in :missions") - List findMissionIn(@Param("missions") List missions); + @Query("SELECT m FROM Mission m WHERE m.id IN :missions ORDER BY m.level, m.title") + List findMissionInOrderByLevelAndTitle(@Param("missions") List missions); } diff --git a/src/main/java/org/sopt/app/interfaces/postgres/StampRepository.java b/src/main/java/org/sopt/app/interfaces/postgres/StampRepository.java index aff77ebe..5d6c50d3 100644 --- a/src/main/java/org/sopt/app/interfaces/postgres/StampRepository.java +++ b/src/main/java/org/sopt/app/interfaces/postgres/StampRepository.java @@ -1,16 +1,16 @@ package org.sopt.app.interfaces.postgres; +import java.util.List; +import java.util.Optional; import org.sopt.app.domain.entity.Stamp; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - public interface StampRepository extends JpaRepository { - List findAllByUserId(Long userId); + List findAllByUserId(Long userId); - Stamp findByUserIdAndMissionId(Long userId, Long missionId); + Optional findByUserIdAndMissionId(Long userId, Long missionId); - void deleteAllByUserId(Long userId); + void deleteAllByUserId(Long userId); } diff --git a/src/main/java/org/sopt/app/interfaces/postgres/UserRepository.java b/src/main/java/org/sopt/app/interfaces/postgres/UserRepository.java index 8620c29f..800d1d4d 100644 --- a/src/main/java/org/sopt/app/interfaces/postgres/UserRepository.java +++ b/src/main/java/org/sopt/app/interfaces/postgres/UserRepository.java @@ -1,14 +1,14 @@ package org.sopt.app.interfaces.postgres; +import java.util.Optional; import org.sopt.app.domain.entity.User; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - public interface UserRepository extends JpaRepository { - Optional findUserByEmail(String email); Optional findUserByNickname(String nickname); Optional findUserById(Long userId); + + Optional findUserByPlaygroundId(Long playgroundId); } diff --git a/src/main/java/org/sopt/app/presentation/HealthCheckController.java b/src/main/java/org/sopt/app/presentation/HealthCheckController.java new file mode 100644 index 00000000..c14e9322 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/HealthCheckController.java @@ -0,0 +1,21 @@ +package org.sopt.app.presentation; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@RequestMapping("/api/v2/health") +@RequiredArgsConstructor +public class HealthCheckController { + + @Operation(summary = "Health Check") + @GetMapping(value = "") + @ResponseBody + public String healthCheck() { + return "Healthy!!"; + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/presentation/auth/AuthController.java b/src/main/java/org/sopt/app/presentation/auth/AuthController.java index 1c56c3fc..760f90fb 100644 --- a/src/main/java/org/sopt/app/presentation/auth/AuthController.java +++ b/src/main/java/org/sopt/app/presentation/auth/AuthController.java @@ -1,58 +1,71 @@ package org.sopt.app.presentation.auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import javax.validation.Valid; import lombok.RequiredArgsConstructor; -import org.sopt.app.application.auth.AuthUseCaseImpl; -import org.sopt.app.presentation.auth.dto.ChangeNicknameRequestDto; -import org.sopt.app.presentation.auth.dto.ChangePasswordRequestDto; -import org.springframework.web.bind.annotation.*; +import lombok.val; +import org.sopt.app.application.auth.JwtTokenService; +import org.sopt.app.application.auth.PlaygroundAuthService; +import org.sopt.app.application.user.UserService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor +@RequestMapping("/api/v2/auth") public class AuthController { - private final AuthUseCaseImpl authUseCase; + private final PlaygroundAuthService playgroundAuthService; + private final UserService userService; + private final JwtTokenService jwtTokenService; + private final AuthResponseMapper authResponseMapper; - /** - * ๋‹‰๋„ค์ž„, ์ด๋ฉ”์ผ ๊ฒ€์ฆ API - */ - @GetMapping(value = "/api/v1/auth") - public void check(@RequestParam(value = "nickname", required = false) String nickname, - @RequestParam(value = "email", required = false) String email) { - authUseCase.validate(nickname, email); - } - - /** - * ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ - */ + @Operation(summary = "ํ”Œ๊ทธ๋กœ ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž…") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "401", description = "token error", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @PostMapping(value = "/playground") + public ResponseEntity playgroundLogin(@Valid @RequestBody AuthRequest.CodeRequest codeRequest) { + val temporaryToken = playgroundAuthService.getPlaygroundAccessToken(codeRequest); + val playgroundToken = playgroundAuthService.refreshPlaygroundToken(temporaryToken); + val playgroundMember = playgroundAuthService.getPlaygroundInfo(playgroundToken.getAccessToken()); + val userId = userService.loginWithUserPlaygroundId(playgroundMember); - @PatchMapping(value = "/api/v1/auth/password") - public void changePassword( - @RequestHeader(name = "userId") String userId, - @RequestBody ChangePasswordRequestDto changePasswordRequestDto - ) { - authUseCase.changePassword(userId, changePasswordRequestDto.getPassword()); + val appToken = jwtTokenService.issueNewTokens(userId, playgroundMember); + val response = authResponseMapper.of(appToken.getAccessToken(), appToken.getRefreshToken(), + playgroundMember.getAccessToken(), playgroundMember.getStatus()); + return ResponseEntity.status(HttpStatus.OK).body(response); } - /** - * ๋‹‰๋„ค์ž„ ๋ณ€๊ฒฝ - */ - @PatchMapping(value = "/api/v1/auth/nickname") - public void changeNickname( - @RequestHeader(name = "userId") String userId, - @RequestBody ChangeNicknameRequestDto changeNicknameRequestDto + @Operation(summary = "ํ† ํฐ ๋ฆฌํ”„๋ ˆ์‹œ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "401", description = "token error", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @PatchMapping(value = "/refresh") + public ResponseEntity refreshToken( + @Valid @RequestBody AuthRequest.RefreshRequest refreshRequest ) { - String nickname = changeNicknameRequestDto.getNickname(); - authUseCase.changeNickname(userId, nickname); - } + val userId = jwtTokenService.getUserIdFromJwtToken(refreshRequest.getRefreshToken()); + val existingToken = userService.getPlaygroundToken(userId); + val playgroundToken = playgroundAuthService.refreshPlaygroundToken(existingToken); + val playgroundMember = playgroundAuthService.getPlaygroundInfo(playgroundToken.getAccessToken()); - /** - * ํƒˆํ‡ดํ•˜๊ธฐ - */ - @DeleteMapping(value = "/api/v1/auth/withdraw") - public void withdraw( - @RequestHeader(name = "userId") String userId - ) { - authUseCase.deleteUser(userId); + val appToken = jwtTokenService.issueNewTokens(userId, playgroundMember); + val response = authResponseMapper.of(appToken.getAccessToken(), appToken.getRefreshToken(), + playgroundToken.getAccessToken(), playgroundMember.getStatus()); + return ResponseEntity.status(HttpStatus.OK).body(response); } } \ No newline at end of file diff --git a/src/main/java/org/sopt/app/presentation/auth/AuthRequest.java b/src/main/java/org/sopt/app/presentation/auth/AuthRequest.java new file mode 100644 index 00000000..18bedf51 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/auth/AuthRequest.java @@ -0,0 +1,38 @@ +package org.sopt.app.presentation.auth; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +public class AuthRequest { + + @Getter + @Setter + @ToString + public static class CodeRequest { + + @Schema(description = "ํ”Œ๋ ˆ์ด๊ทธ๋ผ์šด๋“œ OAuth Token", example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyMiIsImV4cCI6MTY4MDAxNDQzNn0.asdfasdfasdfasdfasdfasdf") + @NotNull(message = "code may not be null") + private String code; + } + + @Getter + @Setter + @ToString + public static class AccessTokenRequest { + + private String accessToken; + } + + @Getter + @Setter + @ToString + public static class RefreshRequest { + + @Schema(description = "์•ฑ Refresh Token", example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyMiIsImV4cCI6MTY4MDAxNDQzNn0.asdfasdfasdfasdfasdfasdf") + @NotNull(message = "code may not be null") + private String refreshToken; + } +} diff --git a/src/main/java/org/sopt/app/presentation/auth/AuthResponse.java b/src/main/java/org/sopt/app/presentation/auth/AuthResponse.java new file mode 100644 index 00000000..b62de568 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/auth/AuthResponse.java @@ -0,0 +1,34 @@ +package org.sopt.app.presentation.auth; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.sopt.app.domain.enums.UserStatus; + +public class AuthResponse { + + @Getter + @Setter + @ToString + public static class LoginResponse { + + private Long userId; + private String profileMessage; + } + + @Getter + @Setter + @ToString + public static class Token { + + @Schema(description = "์•ฑ ์„œ๋ฒ„ AccessToken", example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyMiIsImV4cCI6MTY4MDAxNDQzNn0.asdfasdfasdfasdfasdfasdf") + private String accessToken; + @Schema(description = "์•ฑ ์„œ๋ฒ„ RefreshToken", example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyMiIsImV4cCI6MTY4MDAxNDQzNn0.asdfasdfasdfasdfasdfasdf") + private String refreshToken; + @Schema(description = "ํ”Œ๋ ˆ์ด๊ทธ๋ผ์šด๋“œ AccessToken", example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyMiIsImV4cCI6MTY4MDAxNDQzNn0.asdfasdfasdfasdfasdfasdf") + private String playgroundToken; + @Schema(description = "ํ™œ๋™/๋น„ํ™œ๋™/๋น„ํšŒ์› ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ", example = "ACTIVE") + private UserStatus status; + } +} diff --git a/src/main/java/org/sopt/app/presentation/auth/AuthResponseMapper.java b/src/main/java/org/sopt/app/presentation/auth/AuthResponseMapper.java new file mode 100644 index 00000000..693c7532 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/auth/AuthResponseMapper.java @@ -0,0 +1,16 @@ +package org.sopt.app.presentation.auth; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.sopt.app.domain.enums.UserStatus; + +@Mapper( + componentModel = "spring", + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + unmappedTargetPolicy = ReportingPolicy.ERROR +) +public interface AuthResponseMapper { + + AuthResponse.Token of(String accessToken, String refreshToken, String playgroundToken, UserStatus status); +} diff --git a/src/main/java/org/sopt/app/presentation/config/ConfigController.java b/src/main/java/org/sopt/app/presentation/config/ConfigController.java new file mode 100644 index 00000000..c4df1dfd --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/config/ConfigController.java @@ -0,0 +1,33 @@ +package org.sopt.app.presentation.config; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.sopt.app.presentation.config.ConfigResponse.Availability; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v2/config") +public class ConfigController { + + @Value("${makers.app.url.is-available}") + private Boolean isAvailable; + + @Operation(summary = "์•ฑ ๋ฉ”์ธ ๋ทฐ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @GetMapping(value = "/availability") + public ResponseEntity getUserInfo() { + return ResponseEntity.status(HttpStatus.OK).body(Availability.builder().isAvailable(isAvailable).build()); + } +} diff --git a/src/main/java/org/sopt/app/presentation/config/ConfigResponse.java b/src/main/java/org/sopt/app/presentation/config/ConfigResponse.java new file mode 100644 index 00000000..d5b15690 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/config/ConfigResponse.java @@ -0,0 +1,19 @@ +package org.sopt.app.presentation.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +public class ConfigResponse { + + @Getter + @Builder + @ToString + public static class Availability { + + @Schema(description = "์•ฑ ๋ฉ”์ธ ๋ทฐ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ", example = "true") + private Boolean isAvailable; + + } +} diff --git a/src/main/java/org/sopt/app/presentation/firebase/FirebaseController.java b/src/main/java/org/sopt/app/presentation/firebase/FirebaseController.java index 7f6fbbfb..44b4980c 100644 --- a/src/main/java/org/sopt/app/presentation/firebase/FirebaseController.java +++ b/src/main/java/org/sopt/app/presentation/firebase/FirebaseController.java @@ -1,30 +1,35 @@ package org.sopt.app.presentation.firebase; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.RequiredArgsConstructor; -import org.sopt.app.presentation.firebase.dto.FirebaseResponseDto; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor +@RequestMapping("/api/v2/firebase") public class FirebaseController { + @Operation(summary = "firebase ์—ฐ๋™์„ ์œ„ํ•œ ์ •๋ณด GET") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @GetMapping(value = "") + public FirebaseResponse.Main getFirebaseInfo() { - /** - * firebase ์—ฐ๋™์„ ์œ„ํ•œ ์ •๋ณด GET - * @return - */ - @GetMapping(value = "/api/v1/firebase") - public FirebaseResponseDto getfirebaseInfo() { - - return FirebaseResponseDto.builder() - .iosForceUpdateVersion("1.0.0") - .iosAppVersion("1.0.2") - .androidForceUpdateVersion("1.0.0") - .androidAppVersion("1.0.0") - .notice("์•ˆ๋…•ํ•˜์„ธ์š”, makers์ž…๋‹ˆ๋‹ค. \n ํ˜„์žฌ ๋ฏธ์…˜ ์ˆ˜์ •/๋“ฑ๋ก์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ์ด์Šˆ๊ฐ€ ํ™•์ธ๋˜์–ด ์›์ธ ํŒŒ์•… ์ค‘์— ์žˆ์Šต๋‹ˆ๋‹ค. \n ์•ฑ ์ด์šฉ์— ๋ถˆํŽธ์„ ๋“œ๋ฆฐ ์  ์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. \n ๋น ๋ฅธ ์‹œ์ผ ๋‚ด ๋ณต๊ตฌ ํ›„ ์žฌ๊ณต์ง€ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.") - .imgUrl(null) - .build(); - } + return FirebaseResponse.Main.builder() + .iosForceUpdateVersion("1.0.0") + .iosAppVersion("1.0.2") + .androidForceUpdateVersion("1.0.0") + .androidAppVersion("1.0.0") + .notice("์•ˆ๋…•ํ•˜์„ธ์š”, makers์ž…๋‹ˆ๋‹ค. \n ํ˜„์žฌ ๋ฏธ์…˜ ์ˆ˜์ •/๋“ฑ๋ก์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ์ด์Šˆ๊ฐ€ ํ™•์ธ๋˜์–ด ์›์ธ ํŒŒ์•… ์ค‘์— ์žˆ์Šต๋‹ˆ๋‹ค. \n ์•ฑ ์ด์šฉ์— ๋ถˆํŽธ์„ ๋“œ๋ฆฐ ์  ์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. \n ๋น ๋ฅธ ์‹œ์ผ ๋‚ด ๋ณต๊ตฌ ํ›„ ์žฌ๊ณต์ง€ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.") + .imgUrl(null) + .build(); + } } diff --git a/src/main/java/org/sopt/app/presentation/firebase/FirebaseResponse.java b/src/main/java/org/sopt/app/presentation/firebase/FirebaseResponse.java new file mode 100644 index 00000000..5d75d895 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/firebase/FirebaseResponse.java @@ -0,0 +1,34 @@ +package org.sopt.app.presentation.firebase; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + + +public class FirebaseResponse { + + @Getter + @Builder + @ToString + public static class Main { + + @JsonProperty("iOS_force_update_version") + private String iosForceUpdateVersion; + + @JsonProperty("iOS_app_version") + private String iosAppVersion; + + @JsonProperty("android_force_update_version") + private String androidForceUpdateVersion; + + @JsonProperty("android_app_version") + private String androidAppVersion; + + private String notice; + + @JsonProperty("img_url") + private String imgUrl; + } +} diff --git a/src/main/java/org/sopt/app/presentation/mission/MissionController.java b/src/main/java/org/sopt/app/presentation/mission/MissionController.java index b1f6989e..0bb87737 100644 --- a/src/main/java/org/sopt/app/presentation/mission/MissionController.java +++ b/src/main/java/org/sopt/app/presentation/mission/MissionController.java @@ -1,90 +1,83 @@ package org.sopt.app.presentation.mission; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import java.util.List; +import javax.validation.Valid; import lombok.AllArgsConstructor; +import lombok.val; import org.sopt.app.application.mission.MissionService; -import org.sopt.app.common.s3.S3Service; -import org.sopt.app.domain.entity.Mission; -import org.sopt.app.presentation.BaseController; -import org.sopt.app.presentation.mission.dto.MissionRequestDto; -import org.sopt.app.presentation.mission.dto.MissionResponseDto; +import org.sopt.app.domain.entity.User; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import java.util.List; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController @AllArgsConstructor -@RequestMapping("/api/v1/mission") -public class MissionController extends BaseController { +@RequestMapping("/api/v2/mission") +@SecurityRequirement(name = "Authorization") +public class MissionController { private final MissionService missionService; - private final S3Service s3Service; + private final MissionResponseMapper missionResponseMapper; - /** - * ์ „์ฒด mission ์กฐํšŒํ•˜๊ธฐ - */ + @Operation(summary = "๋ฏธ์…˜ ์ „์ฒด ์กฐํšŒํ•˜๊ธฐ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) @GetMapping(value = "/all") - @ResponseBody - public ResponseEntity findAllMission(@RequestHeader("userId") String userId) { - return new ResponseEntity<>(missionService.findAllMission(userId), getSuccessHeaders(), - HttpStatus.OK); + public ResponseEntity> findAllMission(@AuthenticationPrincipal User user) { + val result = missionService.findAllMission(user.getId()); + val response = missionResponseMapper.ofCompleteness(result); + return ResponseEntity.status(HttpStatus.OK).body(response); } - /** - * ๋ฏธ์…˜ ์—…๋กœ๋“œ ํ•˜๊ธฐ - */ - @PostMapping() - public ResponseEntity uploadMission( - @RequestPart("missionContent") MissionRequestDto missionRequestDto, - @RequestPart(name = "imgUrl", required = false) List multipartFiles) { - - //MultipartFile์„ ๋ฆฌ์ŠคํŠธ์— ๋„ฃ์–ด์คฌ๊ธฐ ๋•Œ๋ฌธ์— List ๋‚ด๋ถ€์˜ ์ด๋ฏธ์ง€ํŒŒ์ผ์— isEmpty()๋ฅผ ์ ์šฉํ•ด์•ผ ํ•œ๋‹ค. - int checkNum = 1; - for (MultipartFile image : multipartFiles) { - if (image.isEmpty()) { - checkNum = 0; - } - } - - MissionResponseDto result = MissionResponseDto.builder().build(); - if (checkNum == 0) { - Mission mission = missionService.uploadMission(missionRequestDto); - result.setMissionId(mission.getId()); - } else { - List imgPaths = s3Service.upload(multipartFiles); - Mission uploadMissionWithImg = missionService.uploadMissionWithImg(missionRequestDto, - imgPaths); - result.setMissionId(uploadMissionWithImg.getId()); - } - - return new ResponseEntity<>(result, getSuccessHeaders(), HttpStatus.OK); + @Operation(summary = "๋ฏธ์…˜ ์ƒ์„ฑํ•˜๊ธฐ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @PostMapping("") + public ResponseEntity registerMission( + @Valid @RequestBody MissionRequest.RegisterMissionRequest registerMissionRequest) { + val mission = missionService.uploadMission(registerMissionRequest); + val response = missionResponseMapper.of(mission.getId()); + return ResponseEntity.status(HttpStatus.OK).body(response); } - - /** - * ์™„๋ฃŒ ๋ฏธ์…˜๋งŒ ์กฐํšŒํ•˜๊ธฐ - */ + @Operation(summary = "์™„๋ฃŒ ๋ฏธ์…˜๋งŒ ์กฐํšŒํ•˜๊ธฐ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) @GetMapping("complete") - public ResponseEntity findCompleteMission(@RequestHeader("userId") String userId) { - - List resultMission = missionService.getCompleteMission(userId); - - return new ResponseEntity<>(resultMission, getSuccessHeaders(), HttpStatus.OK); + public ResponseEntity> findCompleteMission(@AuthenticationPrincipal User user) { + val result = missionService.getCompleteMission(user.getId()); + val response = missionResponseMapper.of(result); + return ResponseEntity.status(HttpStatus.OK).body(response); } - /** - * ๋ฏธ์™„๋ฃŒ ๋ฏธ์…˜๋งŒ ์กฐํšŒํ•˜๊ธฐ - */ + @Operation(summary = "๋ฏธ์™„๋ฃŒ ๋ฏธ์…˜๋งŒ ์กฐํšŒํ•˜๊ธฐ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) @GetMapping("incomplete") - public ResponseEntity findInCompleteMission(@RequestHeader("userId") String userId) { - List resultMission = missionService.getIncompleteMission(userId); - - return new ResponseEntity<>(resultMission, getSuccessHeaders(), HttpStatus.OK); + public ResponseEntity> findInCompleteMission(@AuthenticationPrincipal User user) { + val result = missionService.getIncompleteMission(user.getId()); + val response = missionResponseMapper.of(result); + return ResponseEntity.status(HttpStatus.OK).body(response); } - } diff --git a/src/main/java/org/sopt/app/presentation/mission/MissionRequest.java b/src/main/java/org/sopt/app/presentation/mission/MissionRequest.java new file mode 100644 index 00000000..a4620909 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/mission/MissionRequest.java @@ -0,0 +1,26 @@ +package org.sopt.app.presentation.mission; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +public class MissionRequest { + + @Getter + @Setter + @ToString + public static class RegisterMissionRequest { + + @Schema(description = "๋ฏธ์…˜ ์ด๋ฏธ์ง€", example = "https://s3.ap-northeast-2.amazonaws.com/example/283aab53-22e3-46da-85ec-146c99f82ed4") + @NotNull(message = "image may not be null") + private String image; + @Schema(description = "๋ฏธ์…˜ ์ œ๋ชฉ", example = "์•ฑํŒ€ ์ตœ๊ณ ") + @NotNull(message = "title may not be null") + private String title; + @Schema(description = "๋ฏธ์…˜ ๋ ˆ๋ฒจ", example = "3") + @NotNull(message = "level may not be null") + private Integer level; + } +} diff --git a/src/main/java/org/sopt/app/presentation/mission/MissionResponse.java b/src/main/java/org/sopt/app/presentation/mission/MissionResponse.java new file mode 100644 index 00000000..f9d926da --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/mission/MissionResponse.java @@ -0,0 +1,52 @@ +package org.sopt.app.presentation.mission; + + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +public class MissionResponse { + + @Getter + @Setter + @ToString + public static class MissionMain { + + @Schema(description = "๋ฏธ์…˜ ์•„์ด๋””", example = "1") + private Long id; + @Schema(description = "๋ฏธ์…˜ ์ œ๋ชฉ", example = "ํŒ€์› ์นญ์ฐฌํ•˜๊ธฐ") + private String title; + @Schema(description = "๋ฏธ์…˜ ๋ ˆ๋ฒจ", example = "1") + private Integer level; + @Schema(description = "๋ฏธ์…˜ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€", example = "null") + private List profileImage; + } + + @Getter + @Setter + @ToString + public static class Completeness { + + @Schema(description = "๋ฏธ์…˜ ์•„์ด๋””", example = "1") + private Long id; + @Schema(description = "๋ฏธ์…˜ ์ œ๋ชฉ", example = "ํŒ€์› ์นญ์ฐฌํ•˜๊ธฐ") + private String title; + @Schema(description = "๋ฏธ์…˜ ๋ ˆ๋ฒจ", example = "1") + private Integer level; + @Schema(description = "๋ฏธ์…˜ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€", example = "null") + private List profileImage; + @Schema(description = "๋ฏธ์…˜ ์™„๋ฃŒ ์—ฌ๋ถ€", example = "true") + private Boolean isCompleted; + } + + @Getter + @Setter + @ToString + public static class MissionId { + + @Schema(description = "๋ฏธ์…˜ ์•„์ด๋””", example = "1") + private Long missionId; + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/presentation/mission/MissionResponseMapper.java b/src/main/java/org/sopt/app/presentation/mission/MissionResponseMapper.java new file mode 100644 index 00000000..e9de83c0 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/mission/MissionResponseMapper.java @@ -0,0 +1,23 @@ +package org.sopt.app.presentation.mission; + +import java.util.List; +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.sopt.app.application.mission.MissionInfo; +import org.sopt.app.domain.entity.Mission; + +@Mapper( + componentModel = "spring", + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + unmappedTargetPolicy = ReportingPolicy.ERROR +) +public interface MissionResponseMapper { + + MissionResponse.MissionId of(Long missionId); + + List of(List missionList); + + List ofCompleteness(List missionList); + +} diff --git a/src/main/java/org/sopt/app/presentation/rank/RankController.java b/src/main/java/org/sopt/app/presentation/rank/RankController.java index e1049ba4..5fff495d 100644 --- a/src/main/java/org/sopt/app/presentation/rank/RankController.java +++ b/src/main/java/org/sopt/app/presentation/rank/RankController.java @@ -1,44 +1,55 @@ package org.sopt.app.presentation.rank; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import java.util.List; import lombok.RequiredArgsConstructor; +import lombok.val; +import org.sopt.app.application.mission.MissionService; import org.sopt.app.application.rank.RankService; -import org.sopt.app.domain.entity.User; -import org.sopt.app.presentation.BaseController; -import org.sopt.app.presentation.rank.dto.FindAllRanksResponseDto; -import org.sopt.app.presentation.rank.dto.FindRankResponseDto; -import org.sopt.app.presentation.rank.dto.UserProfileRequestDto; -import org.springframework.web.bind.annotation.*; - -import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1/rank") -public class RankController extends BaseController { +@RequestMapping("/api/v2/rank") +@SecurityRequirement(name = "Authorization") +public class RankController { private final RankService rankService; - - /** - * ํ•œ๋งˆ๋”” ํŽธ์ง‘ํ•˜๊ธฐ - */ - @PostMapping("/profileMessage") - public User updateUserProfileMessage( - @RequestHeader Long userId, - @RequestBody UserProfileRequestDto userProfileRequestDto - ) { - - return rankService - .updateProfileMessage(userId, userProfileRequestDto.getProfileMessage()); - } - + private final RankResponseMapper rankResponseMapper; + private final MissionService missionService; + + @Operation(summary = "๋žญํ‚น ๋ชฉ๋ก ์กฐํšŒ") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) @GetMapping("") - public List findRanks() { - return rankService.findRanks(); + public ResponseEntity> findRanks() { + val result = rankService.findRanks(); + val response = rankResponseMapper.of(result); + return ResponseEntity.status(HttpStatus.OK).body(response); } + @Operation(summary = "๋žญํ‚น ์ƒ์„ธ ์กฐํšŒ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "400", description = "no user with the nickname", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) @GetMapping("/detail") - public FindRankResponseDto findRankById( - @RequestHeader Long userId) { - return rankService.findRankById(userId); + public ResponseEntity findRankByNickname(@RequestParam(value = "nickname") String nickname) { + val result = rankService.findRankByNickname(nickname); + val missionList = missionService.getCompleteMission(result.getId()); + val response = rankResponseMapper.of(result, missionList); + return ResponseEntity.status(HttpStatus.OK).body(response); } } diff --git a/src/main/java/org/sopt/app/presentation/rank/RankRequest.java b/src/main/java/org/sopt/app/presentation/rank/RankRequest.java new file mode 100644 index 00000000..ab8997f5 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/rank/RankRequest.java @@ -0,0 +1,5 @@ +package org.sopt.app.presentation.rank; + +public class RankRequest { + +} diff --git a/src/main/java/org/sopt/app/presentation/rank/RankResponse.java b/src/main/java/org/sopt/app/presentation/rank/RankResponse.java new file mode 100644 index 00000000..e9c2f32f --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/rank/RankResponse.java @@ -0,0 +1,55 @@ +package org.sopt.app.presentation.rank; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +public class RankResponse { + + @Getter + @Setter + @ToString + public static class RankMain { + + @Schema(description = "์œ ์ € ๋žญํ‚น ์ˆœ์œ„", example = "1") + private Integer rank; + @Schema(description = "์œ ์ € ๋‹‰๋„ค์ž„", example = "๊น€์•ฑ์งฑ") + private String nickname; + @Schema(description = "์œ ์ € ๋žญํ‚น ์ ์ˆ˜", example = "15") + private Long point; + @Schema(description = "์œ ์ € ํ”„๋กœํ•„ ๋ฉ”์„ธ์ง€", example = "1๋“ฑ์ด ๋˜๊ณ  ๋ง๊ฑฐ์•ผ!") + private String profileMessage; + } + + @Getter + @Setter + @ToString + public static class Detail { + + @Schema(description = "์œ ์ € ๋‹‰๋„ค์ž„", example = "๊น€์•ฑ์งฑ") + private String nickname; + @Schema(description = "์œ ์ € ํ”„๋กœํ•„ ๋ฉ”์„ธ์ง€", example = "1๋“ฑ์ด ๋˜๊ณ  ๋ง๊ฑฐ์•ผ!") + private String profileMessage; + @Schema(description = "์œ ์ € ๋ฏธ์…˜ ๋ฆฌ์ŠคํŠธ", example = "") + private List userMissions; + } + + @Getter + @Setter + @ToString + public static class RankMission { + + @Schema(description = "๋ฏธ์…˜ ์•„์ด๋””", example = "1") + private Long id; + @Schema(description = "๋ฏธ์…˜ ์ œ๋ชฉ", example = "ํŒ€์› ์นญ์ฐฌํ•˜๊ธฐ") + private String title; + @Schema(description = "๋ฏธ์…˜ ๋ ˆ๋ฒจ", example = "1") + private Integer level; + @Schema(description = "๋ฏธ์…˜ ๋…ธ์ถœ ์—ฌ๋ถ€", example = "true") + private Boolean display; + @Schema(description = "๋ฏธ์…˜ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€", example = "null") + private List profileImage; + } +} diff --git a/src/main/java/org/sopt/app/presentation/rank/RankResponseMapper.java b/src/main/java/org/sopt/app/presentation/rank/RankResponseMapper.java new file mode 100644 index 00000000..aef04d79 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/rank/RankResponseMapper.java @@ -0,0 +1,21 @@ +package org.sopt.app.presentation.rank; + +import java.util.List; +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.sopt.app.application.rank.RankInfo.Main; +import org.sopt.app.domain.entity.Mission; +import org.sopt.app.domain.entity.User; + +@Mapper( + componentModel = "spring", + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + unmappedTargetPolicy = ReportingPolicy.ERROR +) +public interface RankResponseMapper { + + List of(List
rank); + + RankResponse.Detail of(User user, List userMissions); +} diff --git a/src/main/java/org/sopt/app/presentation/s3/S3Controller.java b/src/main/java/org/sopt/app/presentation/s3/S3Controller.java new file mode 100644 index 00000000..328170ad --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/s3/S3Controller.java @@ -0,0 +1,53 @@ +package org.sopt.app.presentation.s3; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.sopt.app.application.s3.S3Service; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v2/s3") +@SecurityRequirement(name = "Authorization") +public class S3Controller { + + private final S3Service s3Service; + private final S3ResponseMapper s3ResponseMapper; + private RestTemplate restTemplate = new RestTemplate(); + + @Operation(summary = "์Šคํƒฌํ”„ pre-signed url ์กฐํšŒ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "400", description = "url generator error", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @GetMapping(value = "/stamp") + public ResponseEntity getStampPreSignedUrl() { + val result = s3Service.getPreSignedUrl("stamp"); + val response = s3ResponseMapper.of(result); + return ResponseEntity.status(HttpStatus.OK).body(response); + } + + @Operation(summary = "๋ฏธ์…˜ pre-signed url ์กฐํšŒ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "400", description = "url generator error", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @GetMapping(value = "/mission") + public ResponseEntity getMissionPreSignedUrl() { + val result = s3Service.getPreSignedUrl("mission"); + val response = s3ResponseMapper.of(result); + return ResponseEntity.status(HttpStatus.OK).body(response); + } +} diff --git a/src/main/java/org/sopt/app/presentation/s3/S3Response.java b/src/main/java/org/sopt/app/presentation/s3/S3Response.java new file mode 100644 index 00000000..f1bdb175 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/s3/S3Response.java @@ -0,0 +1,20 @@ +package org.sopt.app.presentation.s3; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +public class S3Response { + + @Getter + @Setter + @ToString + public static class PreSignedUrl { + + @Schema(description = "Pre-Signed URL ์ฃผ์†Œ", example = "https://s3.ap-northeast-2.amazonaws.com/example/6d7cfc58-05a6-4a83-9112-725f3f95d4fd?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230412T155024Z&X-Amz-SignedHeaders=host&X-Amz-Expires=3599&X-Amz-Credential=AKIAVQPX6FSLOVJS53KL%2F20230412%2Fap-northeast-2%2Fs3%2Faws4_request&X-Amz-Signature=123456123456") + private String preSignedURL; + @Schema(description = "์—…๋กœ๋“œ ๋  Image URL ์ฃผ์†Œ", example = "https://sopt-makers.s3.ap-northeast-2.amazonaws.com/example/6d7cfc58-05a6-4a83-9112-725f3f95d4fd") + private String imageURL; + } +} diff --git a/src/main/java/org/sopt/app/presentation/s3/S3ResponseMapper.java b/src/main/java/org/sopt/app/presentation/s3/S3ResponseMapper.java new file mode 100644 index 00000000..039d2598 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/s3/S3ResponseMapper.java @@ -0,0 +1,17 @@ +package org.sopt.app.presentation.s3; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.sopt.app.application.s3.S3Info; + +@Mapper( + componentModel = "spring", + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + unmappedTargetPolicy = ReportingPolicy.ERROR +) +public interface S3ResponseMapper { + + S3Response.PreSignedUrl of(S3Info.PreSignedUrl url); + +} diff --git a/src/main/java/org/sopt/app/presentation/stamp/StampController.java b/src/main/java/org/sopt/app/presentation/stamp/StampController.java index 6cfcd4bc..11fa75c7 100644 --- a/src/main/java/org/sopt/app/presentation/stamp/StampController.java +++ b/src/main/java/org/sopt/app/presentation/stamp/StampController.java @@ -1,110 +1,148 @@ package org.sopt.app.presentation.stamp; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import java.util.List; +import javax.validation.Valid; import lombok.AllArgsConstructor; +import lombok.val; +import org.sopt.app.application.s3.S3Service; import org.sopt.app.application.stamp.StampService; -import org.sopt.app.common.s3.S3Service; -import org.sopt.app.domain.entity.Stamp; -import org.sopt.app.presentation.BaseController; -import org.sopt.app.presentation.stamp.dto.StampRequestDto; -import org.sopt.app.presentation.stamp.dto.StampResponseDto; +import org.sopt.app.domain.entity.User; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.util.List; - @RestController @AllArgsConstructor -@RequestMapping("/api/v1/stamp") -public class StampController extends BaseController { - - private final StampService stampService; - - private final S3Service s3Service; - - - /** - * stamp ์กฐํšŒ missionId, userId๋กœ stamp ์กฐํšŒํ•˜๊ธฐ - */ - @GetMapping("/{missionId}") - public Stamp findStampByMissionAndUserId( - @RequestHeader("userId") String userId, - @PathVariable Long missionId) { - - return stampService.findStamp(userId, missionId); - } - - /** - * ๋ฏธ์…˜ ๋‹ฌ์„ฑ์„ ์œ„ํ•ด ๋‚ด์šฉ์„ Stamp ๋‚ด์šฉ ๋“ฑ๋ก - */ - @PostMapping("/{missionId}") - public ResponseEntity uploadStamp( - @RequestHeader("userId") String userId, - @PathVariable Long missionId, - @RequestPart("stampContent") StampRequestDto stampRequestDto, - @RequestPart(name = "imgUrl", required = false) List multipartFiles - ) { - - //์Šคํƒฌํ”„ ์ค‘๋ณต ๊ฒ€์‚ฌ์ฒดํฌ - stampService.checkDuplicateStamp(userId, missionId); - - List imgPaths = s3Service.upload(multipartFiles); - Stamp uploadStamp = stampService.uploadStamp(stampRequestDto, - imgPaths, userId, missionId); - - return new ResponseEntity<>(uploadStamp, getSuccessHeaders(), HttpStatus.OK); - } - - - @PutMapping("/{missionId}") - public ResponseEntity editStamp( - @RequestHeader("userId") String userId, - @PathVariable Long missionId, - @RequestPart(value = "stampContent", required = false) StampRequestDto stampRequestDto, - @RequestPart(name = "imgUrl", required = false) List multipartFiles - - ) { - //MultipartFile์„ ๋ฆฌ์ŠคํŠธ์— ๋„ฃ์–ด์คฌ๊ธฐ ๋•Œ๋ฌธ์— List ๋‚ด๋ถ€์˜ ์ด๋ฏธ์ง€ํŒŒ์ผ์— isEmpty()๋ฅผ ์ ์šฉํ•ด์•ผ ํ•œ๋‹ค. - int checkNum = 1; - for (MultipartFile image : multipartFiles) { - if (image.isEmpty()) { - checkNum = 0; - } +@RequestMapping("/api/v2/stamp") +@SecurityRequirement(name = "Authorization") +public class StampController { + + private final StampService stampService; + + private final S3Service s3Service; + + private final StampResponseMapper stampResponseMapper; + + @Operation(summary = "์Šคํƒฌํ”„ ์กฐํšŒํ•˜๊ธฐ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "400", description = "no stamp", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @GetMapping("/mission/{missionId}") + public ResponseEntity findStampByMissionAndUserId( + @AuthenticationPrincipal User user, + @PathVariable Long missionId + ) { + val result = stampService.findStamp(user.getId(), missionId); + val response = stampResponseMapper.of(result); + return ResponseEntity.status(HttpStatus.OK).body(response); } - StampResponseDto result = StampResponseDto.builder().build(); - if (checkNum == 0) { + @Operation(summary = "์Šคํƒฌํ”„ ๋“ฑ๋กํ•˜๊ธฐ - DEPRECATED") + @PostMapping("/{missionId}") + public ResponseEntity registerStampDeprecated( + @AuthenticationPrincipal User user, + @PathVariable Long missionId, + @RequestPart("stampContent") StampRequest.RegisterStampRequest registerStampRequest, + @RequestPart(name = "imgUrl", required = false) List multipartFiles + ) { + stampService.checkDuplicateStamp(user.getId(), missionId); + val imgPaths = s3Service.uploadDeprecated(multipartFiles); + val result = stampService.uploadStampDeprecated(registerStampRequest, imgPaths, user.getId(), missionId); + val response = stampResponseMapper.of(result); + return ResponseEntity.status(HttpStatus.OK).body(response); + } - Stamp stamp = stampService.editStampContents(stampRequestDto, userId, missionId); - result.setStampId(stamp.getId()); + @Operation(summary = "์Šคํƒฌํ”„ ์ˆ˜์ •ํ•˜๊ธฐ - DEPRECATED") + @PutMapping("/{missionId}") + public ResponseEntity editStampDeprecated( + @AuthenticationPrincipal User user, + @PathVariable Long missionId, + @RequestPart(value = "stampContent", required = false) StampRequest.EditStampRequest editStampRequest, + @RequestPart(name = "imgUrl", required = false) List multipartFiles + ) { + val stamp = stampService.editStampContentsDeprecated(editStampRequest, user.getId(), missionId); + val imgPaths = s3Service.uploadDeprecated(multipartFiles); + if (imgPaths.size() > 0) { + stampService.editStampImagesDeprecated(stamp, imgPaths); + } + val response = stampResponseMapper.of(stamp.getId()); + return ResponseEntity.status(HttpStatus.OK).body(response); + } - } else { - List imgPaths = s3Service.upload(multipartFiles); - Stamp uploadStamp = stampService.editStampWithImg(stampRequestDto, - imgPaths, userId, missionId); + @Operation(summary = "์Šคํƒฌํ”„ ๋“ฑ๋กํ•˜๊ธฐ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "400", description = "no mission / duplicate stamp", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @PostMapping("/mission/{missionId}") + public ResponseEntity registerStamp( + @AuthenticationPrincipal User user, + @PathVariable Long missionId, + @Valid @RequestBody StampRequest.RegisterStampRequest registerStampRequest + ) { + stampService.checkDuplicateStamp(user.getId(), missionId); + val result = stampService.uploadStamp(registerStampRequest, user, missionId); + val response = stampResponseMapper.of(result); + return ResponseEntity.status(HttpStatus.OK).body(response); + } - result.setStampId(uploadStamp.getId()); + @Operation(summary = "์Šคํƒฌํ”„ ์ˆ˜์ •ํ•˜๊ธฐ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "400", description = "no stamp", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @PutMapping("/mission/{missionId}") + public ResponseEntity editStamp( + @AuthenticationPrincipal User user, + @PathVariable Long missionId, + @Valid @RequestBody StampRequest.EditStampRequest editStampRequest + ) { + val stamp = stampService.editStampContents(editStampRequest, user.getId(), missionId); + val response = stampResponseMapper.of(stamp.getId()); + return ResponseEntity.status(HttpStatus.OK).body(response); } - return new ResponseEntity<>(result, getSuccessHeaders(), HttpStatus.OK); - } - /** - * Stamp ๊ฐœ๋ณ„ ์‚ญ์ œ - */ - @DeleteMapping("/{stampId}") - public ResponseEntity deleteStampById(@PathVariable Long stampId) { - stampService.deleteByStampId(stampId); - return new ResponseEntity<>("{}", getSuccessHeaders(), HttpStatus.OK); - } + @Operation(summary = "์Šคํƒฌํ”„ ์‚ญ์ œํ•˜๊ธฐ(๊ฐœ๋ณ„)") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success", content = @Content), + @ApiResponse(responseCode = "400", description = "no stamp / no mission", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @DeleteMapping("/{stampId}") + public ResponseEntity deleteStampById(@AuthenticationPrincipal User user, + @PathVariable Long stampId) { + stampService.deleteStampById(user, stampId); + return ResponseEntity.status(HttpStatus.OK).body(null); + } - /** - * ์ „์ฒด Stamp์‚ญ์ œ - */ - @DeleteMapping("/all") - public ResponseEntity deleteStampByUserId(@RequestHeader Long userId){ - stampService.deleteStampByUserId(userId); - return new ResponseEntity<>("{}", getSuccessHeaders(), HttpStatus.OK); - } + @Operation(summary = "์Šคํƒฌํ”„ ์‚ญ์ œํ•˜๊ธฐ(์ „์ฒด)") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @DeleteMapping("/all") + public ResponseEntity deleteStampByUserId(@AuthenticationPrincipal User user) { + stampService.deleteAllStamps(user); + return ResponseEntity.status(HttpStatus.OK).body(null); + } } diff --git a/src/main/java/org/sopt/app/presentation/stamp/StampRequest.java b/src/main/java/org/sopt/app/presentation/stamp/StampRequest.java new file mode 100644 index 00000000..9c79c019 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/stamp/StampRequest.java @@ -0,0 +1,36 @@ +package org.sopt.app.presentation.stamp; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +public class StampRequest { + + @Getter + @Setter + @ToString + public static class RegisterStampRequest { + + @Schema(description = "์Šคํƒฌํ”„ ์ด๋ฏธ์ง€", example = "https://s3.ap-northeast-2.amazonaws.com/example/283aab53-22e3-46da-85ec-146c99f82ed4.jpeg") + @NotNull(message = "image may not be null") + private String image; + @Schema(description = "์Šคํƒฌํ”„ ๋‚ด์šฉ", example = "์Šคํƒฌํ”„ ์ฐ์—ˆ๋‹ค!") + @NotNull(message = "contents may not be null") + private String contents; + } + + @Getter + @Setter + @ToString + public static class EditStampRequest { + + @Schema(description = "์Šคํƒฌํ”„ ์ด๋ฏธ์ง€", example = "https://s3.ap-northeast-2.amazonaws.com/example/283aab53-22e3-46da-85ec-146c99f82ed4") + @NotNull(message = "image may not be null") + private String image; + @Schema(description = "์Šคํƒฌํ”„ ๋‚ด์šฉ", example = "์Šคํƒฌํ”„ ์ฐ์—ˆ๋‹ค!") + @NotNull(message = "contents may not be null") + private String contents; + } +} diff --git a/src/main/java/org/sopt/app/presentation/stamp/StampResponse.java b/src/main/java/org/sopt/app/presentation/stamp/StampResponse.java new file mode 100644 index 00000000..afcd28bd --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/stamp/StampResponse.java @@ -0,0 +1,39 @@ +package org.sopt.app.presentation.stamp; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +public class StampResponse { + + @Getter + @Setter + @ToString + public static class StampMain { + + @Schema(description = "์Šคํƒฌํ”„ ์•„์ด๋””", example = "1") + private Long id; + @Schema(description = "์Šคํƒฌํ”„ ๋‚ด์šฉ", example = "๋ชจ๊ฐ๊ณตํ–ˆ๋‹ค!") + private String contents; + @Schema(description = "์Šคํƒฌํ”„ ์ด๋ฏธ์ง€", example = "[https://s3.ap-northeast-2.amazonaws.com/example/283aab53-22e3-46da-85ec-146c99f82ed4.jpeg]") + private List images; + @Schema(description = "์Šคํƒฌํ”„ ์ƒ์„ฑ ์ผ์‹œ", example = "2023-03-29T18:39:42.106369") + private LocalDateTime createdAt; + @Schema(description = "์Šคํƒฌํ”„ ์ˆ˜์ • ์ผ์‹œ", example = "2023-03-29T18:39:42.106369") + private LocalDateTime updatedAt; + @Schema(description = "๋ฏธ์…˜ ์•„์ด๋””", example = "3") + private Long missionId; + } + + @Getter + @Setter + @ToString + public static class StampId { + + @Schema(description = "์Šคํƒฌํ”„ ์•„์ด๋””", example = "1") + private Long stampId; + } +} diff --git a/src/main/java/org/sopt/app/presentation/stamp/StampResponseMapper.java b/src/main/java/org/sopt/app/presentation/stamp/StampResponseMapper.java new file mode 100644 index 00000000..15dac25a --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/stamp/StampResponseMapper.java @@ -0,0 +1,19 @@ +package org.sopt.app.presentation.stamp; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.sopt.app.domain.entity.Stamp; + +@Mapper( + componentModel = "spring", + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + unmappedTargetPolicy = ReportingPolicy.ERROR +) +public interface StampResponseMapper { + + StampResponse.StampMain of(Stamp stamp); + + StampResponse.StampId of(Long stampId); + +} diff --git a/src/main/java/org/sopt/app/presentation/user/UserController.java b/src/main/java/org/sopt/app/presentation/user/UserController.java index f9ace0ea..d71ed5ca 100644 --- a/src/main/java/org/sopt/app/presentation/user/UserController.java +++ b/src/main/java/org/sopt/app/presentation/user/UserController.java @@ -1,59 +1,98 @@ package org.sopt.app.presentation.user; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import javax.validation.Valid; import lombok.RequiredArgsConstructor; -import org.sopt.app.application.user.UserUseCase; -import org.sopt.app.application.user.dto.LogInUserDto; -import org.sopt.app.application.user.dto.SignUpUserDto; +import lombok.val; +import org.sopt.app.application.user.UserService; import org.sopt.app.domain.entity.User; -import org.sopt.app.presentation.user.request.LogInUserRequest; -import org.sopt.app.presentation.user.response.LoginResponse; -import org.sopt.app.presentation.user.response.SignUpResponse; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import javax.validation.Valid; - @RestController @RequiredArgsConstructor +@RequestMapping("/api/v2/user") +@SecurityRequirement(name = "Authorization") public class UserController { - private final UserUseCase userUseCase; + private final UserService userService; + private final UserResponseMapper userResponseMapper; - /** - * ์ด๋ฉ”์ผ ํšŒ์›๊ฐ€์ž… - */ - @PostMapping(value = "/api/v1/user/signup") - public SignUpResponse signUp2(@RequestBody SignUpUserDto request) { - Long userId = userUseCase.signUp(SignUpUserDto.builder() - .nickname(request.getNickname()) - .email(request.getEmail()) - .password(request.getPassword()) - .osType(request.getOsType()) - .clientToken(request.getClientToken()) - .build()); + @Operation(summary = "์œ ์ € ์ •๋ณด ์กฐํšŒ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @GetMapping(value = "") + public ResponseEntity getUserInfo(@AuthenticationPrincipal User user) { + val response = userResponseMapper.ofAppUser(user); + return ResponseEntity.status(HttpStatus.OK).body(response); + } + @Operation(summary = "์†ํƒฌํ”„ ์ •๋ณด ์กฐํšŒ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @GetMapping(value = "/soptamp") + public ResponseEntity getSoptampInfo(@AuthenticationPrincipal User user) { + val response = userResponseMapper.ofSoptamp(user); + return ResponseEntity.status(HttpStatus.OK).body(response); + } - return SignUpResponse.builder() - .userId(userId) - .build(); + @Operation(summary = "๋‹‰๋„ค์ž„ ์ค‘๋ณต ๊ฒ€์‚ฌ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success", content = @Content), + @ApiResponse(responseCode = "400", description = "duplicate nickname", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @GetMapping(value = "/nickname/{nickname}") + public ResponseEntity validateUserNickname(@PathVariable String nickname) { + userService.checkUserNickname(nickname); + return ResponseEntity.status(HttpStatus.OK).body(null); } + @Operation(summary = "๋‹‰๋„ค์ž„ ๋ณ€๊ฒฝ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @PatchMapping(value = "/nickname") + public ResponseEntity editNickname( + @AuthenticationPrincipal User user, + @Valid @RequestBody UserRequest.EditNicknameRequest editNicknameRequest + ) { + val nickname = editNicknameRequest.getNickname(); + val result = userService.editNickname(user, nickname); + val response = userResponseMapper.of(result); + return ResponseEntity.status(HttpStatus.OK).body(response); + } - /** - * ๋กœ๊ทธ์ธ - */ - @PostMapping(value = "/api/v1/user/login") - public LoginResponse signIn2(@Valid @RequestBody LogInUserRequest request){ - - User user = userUseCase.logIn(LogInUserDto.builder() - .email(request.getEmail()) - .password(request.getPassword()) - .build()); - return LoginResponse.builder() - .userId(user.getId()) - .profileMessage(user.getProfileMessage()) - .build(); + @Operation(summary = "ํ•œ๋งˆ๋”” ๋ณ€๊ฒฝ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @PatchMapping("/profile-message") + public ResponseEntity editProfileMessage( + @AuthenticationPrincipal User user, + @Valid @RequestBody UserRequest.EditProfileMessageRequest editProfileMessageRequest + ) { + val result = userService.editProfileMessage(user, editProfileMessageRequest.getProfileMessage()); + val response = userResponseMapper.of(result); + return ResponseEntity.status(HttpStatus.OK).body(response); } + } diff --git a/src/main/java/org/sopt/app/presentation/user/UserOriginalController.java b/src/main/java/org/sopt/app/presentation/user/UserOriginalController.java new file mode 100644 index 00000000..42dfe077 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/user/UserOriginalController.java @@ -0,0 +1,49 @@ +package org.sopt.app.presentation.user; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.sopt.app.application.auth.PlaygroundAuthService; +import org.sopt.app.application.operation.OperationInfo; +import org.sopt.app.application.operation.OperationService; +import org.sopt.app.domain.entity.User; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v2/user") +@SecurityRequirement(name = "Authorization") +public class UserOriginalController { + + private final PlaygroundAuthService playgroundAuthService; + private final OperationService operationService; + private final UserResponseMapper userResponseMapper; + + @Operation(summary = "๋ฉ”์ธ ๋ทฐ ์กฐํšŒ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success"), + @ApiResponse(responseCode = "400", description = "no playground, operation profile", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @GetMapping(value = "/main") + public ResponseEntity getMainViewInfo( + @AuthenticationPrincipal User user, + @RequestHeader("Authorization") String accessToken + ) { + val mainViewUser = playgroundAuthService.getPlaygroundUserForMainView(user.getPlaygroundToken()); +// val mainViewOperation = operationService.getOperationForMainView(accessToken); + val dummyOperation = OperationInfo.MainView.builder().announcement("๊ณต์ง€๋‹ค!").attendanceScore(2D).build(); + val response = userResponseMapper.ofMainView(mainViewUser, dummyOperation); + return ResponseEntity.status(HttpStatus.OK).body(response); + } +} diff --git a/src/main/java/org/sopt/app/presentation/user/UserRequest.java b/src/main/java/org/sopt/app/presentation/user/UserRequest.java new file mode 100644 index 00000000..31018657 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/user/UserRequest.java @@ -0,0 +1,30 @@ +package org.sopt.app.presentation.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +public class UserRequest { + + @Getter + @Setter + @ToString + public static class EditNicknameRequest { + + @Schema(description = "๋‹‰๋„ค์ž„", example = "๊น€์•ฑ์งฑ") + @NotNull(message = "nickname may not be null") + private String nickname; + } + + @Getter + @Setter + @ToString + public static class EditProfileMessageRequest { + + @Schema(description = "ํ•œ๋งˆ๋””", example = "1๋“ฑ์ด ๋˜๊ณ  ๋ง๊ฑฐ์•ผ!") + @NotNull(message = "profileMessage may not be null") + private String profileMessage; + } +} diff --git a/src/main/java/org/sopt/app/presentation/user/UserResponse.java b/src/main/java/org/sopt/app/presentation/user/UserResponse.java new file mode 100644 index 00000000..6d4cd113 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/user/UserResponse.java @@ -0,0 +1,96 @@ +package org.sopt.app.presentation.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import org.sopt.app.domain.enums.OsType; + +public class UserResponse { + + @Getter + @Builder + @ToString + public static class MainView { + + @Schema(description = "์œ ์ € ์ธ์ฆ ์ •๋ณด", example = "") + private Playground user; + @Schema(description = "์œ ์ € ์šด์˜ ์„œ๋น„์Šค ์ •๋ณด", example = "") + private Operation operation; + + } + + @Getter + @Builder + @ToString + public static class Playground { + + @Schema(description = "ํ™œ๋™/๋น„ํ™œ๋™/๋น„ํšŒ์› ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ", example = "ACTIVE") + private String status; + @Schema(description = "์œ ์ € ์ด๋ฆ„", example = "๊น€์•ฑ์งฑ") + private String name; + @Schema(description = "์œ ์ € ํ”„๋กœํ•„ ๋ฉ”์„ธ์ง€", example = "1๋“ฑ์ด ๋˜๊ณ  ๋ง๊ฑฐ์•ผ!") + private String profileImage; + @Schema(description = "์œ ์ € ํ™œ๋™ ๊ธฐ์ˆ˜ ์ •๋ณด", example = "[32,30,29]") + private List generationList; + + } + + @Getter + @Builder + @ToString + public static class Operation { + + @Schema(description = "์œ ์ € ์†ํŠธ ์ถœ์„ ์ •๋ณด", example = "2.0") + private Double attendanceScore; + @Schema(description = "์†ํŠธ ๊ณต์ง€", example = "๊ณต์ง€๋‹ค!") + private String announcement; + + } + + + @Getter + @Builder + @ToString + public static class AppUser { + + @Schema(description = "์œ ์ € ๋‹‰๋„ค์ž„", example = "๊น€์•ฑ์งฑ") + private String username; + @Schema(description = "์œ ์ € ํด๋ผ์ด์–ธํŠธ ํ† ํฐ", example = "null") + private String clientToken; + @Schema(description = "์œ ์ € OS ํƒ€์ž…", example = "null") + private OsType osType; + } + + @Getter + @Builder + @ToString + public static class Soptamp { + + @Schema(description = "์œ ์ € ๋‹‰๋„ค์ž„", example = "๊น€์•ฑ์งฑ") + private String nickname; + @Schema(description = "์œ ์ € ๋žญํ‚น ์ ์ˆ˜", example = "15") + private Long points; + @Schema(description = "์œ ์ € ํ”„๋กœํ•„ ๋ฉ”์„ธ์ง€", example = "1๋“ฑ์ด ๋˜๊ณ  ๋ง๊ฑฐ์•ผ!") + private String profileMessage; + } + + @Getter + @Builder + @ToString + public static class Nickname { + + @Schema(description = "์œ ์ € ๋‹‰๋„ค์ž„", example = "๊น€์•ฑ์งฑ") + private String nickname; + } + + @Getter + @Builder + @ToString + public static class ProfileMessage { + + @Schema(description = "์œ ์ € ํ”„๋กœํ•„ ๋ฉ”์„ธ์ง€", example = "1๋“ฑ์ด ๋˜๊ณ  ๋ง๊ฑฐ์•ผ!") + private String profileMessage; + } +} diff --git a/src/main/java/org/sopt/app/presentation/user/UserResponseMapper.java b/src/main/java/org/sopt/app/presentation/user/UserResponseMapper.java new file mode 100644 index 00000000..aa7d5cf5 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/user/UserResponseMapper.java @@ -0,0 +1,27 @@ +package org.sopt.app.presentation.user; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.sopt.app.application.auth.PlaygroundAuthInfo; +import org.sopt.app.application.operation.OperationInfo; +import org.sopt.app.application.user.UserInfo; +import org.sopt.app.domain.entity.User; + +@Mapper( + componentModel = "spring", + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + unmappedTargetPolicy = ReportingPolicy.ERROR +) +public interface UserResponseMapper { + + UserResponse.AppUser ofAppUser(User user); + + UserResponse.MainView ofMainView(PlaygroundAuthInfo.MainView user, OperationInfo.MainView operation); + + UserResponse.Soptamp ofSoptamp(User user); + + UserResponse.Nickname of(UserInfo.Nickname nickname); + + UserResponse.ProfileMessage of(UserInfo.ProfileMessage profileMessage); +} diff --git a/src/main/java/org/sopt/app/presentation/user/UserWithdrawController.java b/src/main/java/org/sopt/app/presentation/user/UserWithdrawController.java new file mode 100644 index 00000000..6e373be0 --- /dev/null +++ b/src/main/java/org/sopt/app/presentation/user/UserWithdrawController.java @@ -0,0 +1,40 @@ +package org.sopt.app.presentation.user; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.RequiredArgsConstructor; +import org.sopt.app.application.stamp.StampService; +import org.sopt.app.application.user.UserService; +import org.sopt.app.domain.entity.User; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v2/user") +@SecurityRequirement(name = "Authorization") +public class UserWithdrawController { + + private final UserService userService; + private final StampService stampService; + + @Operation(summary = "ํƒˆํ‡ดํ•˜๊ธฐ") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "success", content = @Content), + @ApiResponse(responseCode = "500", description = "server error", content = @Content) + }) + @DeleteMapping(value = "") + public ResponseEntity withdraw(@AuthenticationPrincipal User user) { + // TODO: S3 ์ด๋ฏธ์ง€ ์‚ญ์ œ + stampService.deleteAllStamps(user); + userService.deleteUser(user); + return ResponseEntity.status(HttpStatus.OK).body(null); + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/v1/application/auth/AuthUseCaseImpl.java b/src/main/java/org/sopt/app/v1/application/auth/AuthUseCaseImpl.java new file mode 100644 index 00000000..a6a56a75 --- /dev/null +++ b/src/main/java/org/sopt/app/v1/application/auth/AuthUseCaseImpl.java @@ -0,0 +1,64 @@ +package org.sopt.app.v1.application.auth; + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.sopt.app.common.exception.v1.DbException; +import org.sopt.app.common.exception.v1.ExistUserException; +import org.sopt.app.common.exception.v1.UserNotFoundException; +import org.sopt.app.domain.entity.User; +import org.sopt.app.v1.interfaces.postgres.UserRepositoryV1; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AuthUseCaseImpl { + + private final UserRepositoryV1 userRepositoryV1; + private final EncryptService encryptService; + + public void validate(String nickname, String email) { + if (nickname != null) { + validateNickname(nickname); + } + if (email != null) { + validateEmail(email); + } + } + + @Transactional + public void changePassword(String userId, String password) { + User user = userRepositoryV1.findUserById(Long.parseLong(userId)).orElseThrow(); + user.password = encryptService.encode(password); + } + + @Transactional + public void changeNickname(String userId, String nickname) { + User user = userRepositoryV1.findUserById(Long.parseLong(userId)).orElseThrow(); + user.nickname = nickname; + } + + private void validateEmail(String email) throws UserNotFoundException { + Optional user = userRepositoryV1.findUserByEmail(email); + if (user.isPresent()) { + throw new ExistUserException("์ด๋ฏธ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค."); + } + } + + private void validateNickname(String nickname) { + Optional user = userRepositoryV1.findUserByNickname(nickname); + if (user.isPresent()) { + throw new ExistUserException("์‚ฌ์šฉ ์ค‘์ธ ๋‹‰๋„ค์ž„์ž…๋‹ˆ๋‹ค."); + } + + } + + @Transactional + public void deleteUser(String userId) { + try { + userRepositoryV1.deleteById(Long.parseLong(userId)); + } catch (Exception e) { + throw new DbException(e); + } + } +} diff --git a/src/main/java/org/sopt/app/application/auth/EncryptService.java b/src/main/java/org/sopt/app/v1/application/auth/EncryptService.java similarity index 93% rename from src/main/java/org/sopt/app/application/auth/EncryptService.java rename to src/main/java/org/sopt/app/v1/application/auth/EncryptService.java index bcf76130..9bee1c39 100644 --- a/src/main/java/org/sopt/app/application/auth/EncryptService.java +++ b/src/main/java/org/sopt/app/v1/application/auth/EncryptService.java @@ -1,4 +1,4 @@ -package org.sopt.app.application.auth; +package org.sopt.app.v1.application.auth; import org.springframework.security.crypto.bcrypt.BCrypt; import org.springframework.security.crypto.password.PasswordEncoder; diff --git a/src/main/java/org/sopt/app/v1/application/mission/MissionServiceV1.java b/src/main/java/org/sopt/app/v1/application/mission/MissionServiceV1.java new file mode 100644 index 00000000..714aac56 --- /dev/null +++ b/src/main/java/org/sopt/app/v1/application/mission/MissionServiceV1.java @@ -0,0 +1,155 @@ +package org.sopt.app.v1.application.mission; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.sopt.app.domain.entity.Mission; +import org.sopt.app.domain.entity.Stamp; +import org.sopt.app.v1.interfaces.postgres.MissionRepositoryV1; +import org.sopt.app.v1.interfaces.postgres.StampRepositoryV1; +import org.sopt.app.v1.presentation.mission.dto.GetAllMissionResponseDto; +import org.sopt.app.v1.presentation.mission.dto.MissionRequestDto; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class MissionServiceV1 { + + private final MissionRepositoryV1 missionRepositoryV1; + private final StampRepositoryV1 stampRepositoryV1; + + + public List findAllMission(String userId) { + List completedStamps = stampRepositoryV1.findAllByUserId(Long.parseLong(userId)); + List missions = missionRepositoryV1.findAll(); + return missions.stream() + .map(mission -> + GetAllMissionResponseDto.builder() + .id(mission.getId()) + .title(mission.getTitle()) + .level(mission.getLevel()) + .profileImage(mission.getProfileImage()) + .isCompleted(isCompletedMission(mission.getId(), completedStamps)) + .build()) + .sorted(Comparator.comparing(GetAllMissionResponseDto::getLevel)) + .collect(Collectors.toList()); + } + + private Boolean isCompletedMission(Long missionId, List completedStamps) { + return completedStamps.stream().anyMatch( + stamp -> stamp.getMissionId().equals(missionId)); + } + + + // ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ - ์ด๋ฏธ์ง€ ๋ฏธํฌํ•จ + @Transactional + public Mission uploadMission(MissionRequestDto missionRequestDto) { + Mission mission = this.convertMission(missionRequestDto); + return missionRepositoryV1.save(mission); + } + + // ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ - ์ด๋ฏธ์ง€ ํฌํ•จ + @Transactional + public Mission uploadMissionWithImg(MissionRequestDto missionRequestDto, List imgPaths) { + + List imgList = new ArrayList<>(imgPaths); + Mission mission = this.convertMissionImg(missionRequestDto, imgList); + return missionRepositoryV1.save(mission); + + } + + + //Mission ์™„๋ฃŒํ•œ ๋ฏธ์…˜๋งŒ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + @Transactional + public List getCompleteMission(String userId) { + + //ํ—ค๋”์—์„œ ๋ฐ›์€ userId๋กœ Stamp ํ…Œ์ด๋ธ”์—์„œ ๋‹ฌ์„ฑํ•œ ๋ฏธ์…˜๋ฒˆํ˜ธ ๊ฐ€์ ธ์˜ค๊ธฐ + List stampList = stampRepositoryV1.findAllByUserId(Long.valueOf(userId)); + + //๋‹ฌ์„ฑํ•œ ๋ฏธ์…˜๋ฒˆํ˜ธ๋ฆฌ์ŠคํŠธ + List missionIdList = new ArrayList<>(); + for (Stamp stamp : stampList) { + missionIdList.add(stamp.getMissionId()); + } + + return missionRepositoryV1.findMissionIn(missionIdList); + } + +// private void getCompleteMission(List stampList, Mission mission) { +// +// mission. +// +// +//// coupon.updateUseCoupon( +//// couponUsedCommand +//// .getCpnNoList() +//// .stream() +//// .filter((CouponUsed command) -> +//// command.getCouponNumber().equals(coupon.getCouponId())) +//// .findAny().orElseThrow(IllegalArgumentException::new)); +// } + + /** + * ๋ฏธ์™„๋ฃŒ ๋ฏธ์…˜๋งŒ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + */ + @Transactional + public List getIncompleteMission(String userId) { + + //์ „์ฒด ๋ฏธ์…˜ ์กฐํšŒํ•˜๊ธฐ + List missionList = missionRepositoryV1.findAll(); + List allMissionList = new ArrayList<>(); + for (Mission mission : missionList) { + allMissionList.add(mission.getId()); + } + + //stamp์—์„œ userId๋กœ ๋‹ฌ์„ฑํ•œ mission ์กฐํšŒํ•˜๊ธฐ + List stampList = stampRepositoryV1.findAllByUserId(Long.valueOf(userId)); + List completeMissionIdList = new ArrayList<>(); + for (Stamp stamp : stampList) { + completeMissionIdList.add(stamp.getMissionId()); + } + + //๋‘ ๋ฆฌ์ŠคํŠธ ๋น„๊ตํ•ด์„œ ์ค‘๋ณต๊ฐ’ ์ œ๊ฑฐ + List inCompleteIdList = allMissionList.stream() + .filter(all -> completeMissionIdList.stream().noneMatch(Predicate.isEqual(all))) + .toList(); + +// noneMatch: ์ค‘๋ณต X +// anyMatch: ์ค‘๋ณต O +// List oldList = Arrays.asList("1", "2", "3", "4"); +// List newList = Arrays.asList("3", "4", "5", "6"); +// +// List resultList1 = oldList.stream() +// .filter(old -> newList.stream().noneMatch(Predicate.isEqual(old))) +// .collect(Collectors.toList()); +// +// System.out.println(resultList1); // [1, 2] + + return missionRepositoryV1.findMissionIn(inCompleteIdList); + } + + + //Mission Entity ์–‘์‹์— ๋งž๊ฒŒ ๋ฐ์ดํ„ฐ ์„ธํŒ… + private Mission convertMissionImg(MissionRequestDto missionRequestDto, List imgList) { + return Mission.builder() + .title(missionRequestDto.getTitle()) + .level(missionRequestDto.getLevel()) + .display(true) + .profileImage(imgList) + .build(); + } + + private Mission convertMission(MissionRequestDto missionRequestDto) { + + return Mission.builder() + .title(missionRequestDto.getTitle()) + .level(missionRequestDto.getLevel()) + .display(true) + .build(); + } + +} diff --git a/src/main/java/org/sopt/app/v1/application/rank/RankServiceV1.java b/src/main/java/org/sopt/app/v1/application/rank/RankServiceV1.java new file mode 100644 index 00000000..3d8c428f --- /dev/null +++ b/src/main/java/org/sopt/app/v1/application/rank/RankServiceV1.java @@ -0,0 +1,61 @@ +package org.sopt.app.v1.application.rank; + +import static org.sopt.app.common.ResponseCode.INVALID_RESPONSE; + +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.sopt.app.common.exception.v1.ApiException; +import org.sopt.app.domain.entity.User; +import org.sopt.app.v1.application.mission.MissionServiceV1; +import org.sopt.app.v1.interfaces.postgres.UserRepositoryV1; +import org.sopt.app.v1.presentation.rank.dto.FindAllRanksResponseDto; +import org.sopt.app.v1.presentation.rank.dto.FindRankResponseDto; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class RankServiceV1 { + + private final UserRepositoryV1 userRepositoryV1; + + private final MissionServiceV1 missionServiceV1; + + //User ํ•œ๋งˆ๋”” ๋“ฑ๋กํ•˜๊ธฐ + public User updateProfileMessage(Long userId, String profileMessage) { + + User user = userRepositoryV1.findUserById(userId) + .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); + + user.updateProfileMessage(profileMessage); + return userRepositoryV1.save(user); + } + + public List findRanks() { + List users = userRepositoryV1.findAll(); + AtomicInteger rankPoint = new AtomicInteger(1); + return users.stream().sorted( + Comparator.comparing(User::getPoints).reversed()) + .map(user -> FindAllRanksResponseDto.builder() + .rank(rankPoint.getAndIncrement()) + .userId(user.getId()) + .nickname(user.getNickname()) + .point(user.getPoints()) + .profileMessage(user.getProfileMessage()) + .build()) + .collect(Collectors.toList()); + } + + public FindRankResponseDto findRankById(Long userId) { + User user = userRepositoryV1.findUserById(userId).orElseThrow(() -> new ApiException(INVALID_RESPONSE)); + return FindRankResponseDto.builder() + .userId(userId) + .nickname(user.getNickname()) + .profileMessage(user.getProfileMessage()) + .userMissions(missionServiceV1.getCompleteMission(String.valueOf(userId))) + .build(); + } + +} diff --git a/src/main/java/org/sopt/app/v1/application/stamp/StampServiceV1.java b/src/main/java/org/sopt/app/v1/application/stamp/StampServiceV1.java new file mode 100644 index 00000000..dab19c57 --- /dev/null +++ b/src/main/java/org/sopt/app/v1/application/stamp/StampServiceV1.java @@ -0,0 +1,151 @@ +package org.sopt.app.v1.application.stamp; + +import static org.sopt.app.common.ResponseCode.DUPLICATE_STAMP; +import static org.sopt.app.common.ResponseCode.INVALID_RESPONSE; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.sopt.app.common.exception.v1.ApiException; +import org.sopt.app.domain.entity.Mission; +import org.sopt.app.domain.entity.Stamp; +import org.sopt.app.domain.entity.User; +import org.sopt.app.v1.interfaces.postgres.MissionRepositoryV1; +import org.sopt.app.v1.interfaces.postgres.StampRepositoryV1; +import org.sopt.app.v1.interfaces.postgres.UserRepositoryV1; +import org.sopt.app.v1.presentation.stamp.dto.StampRequestDto; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +@Service +@RequiredArgsConstructor +public class StampServiceV1 { + + private final StampRepositoryV1 stampRepositoryV1; + + private final UserRepositoryV1 userRepositoryV1; + + private final MissionRepositoryV1 missionRepositoryV1; + + public Stamp findStamp(String userId, Long missionId) { + return stampRepositoryV1.findByUserIdAndMissionId(Long.valueOf(userId), missionId); + } + + @Transactional + public Stamp uploadStamp(StampRequestDto stampRequestDto, List imgPaths, String userId, + Long missionId) { + List imgList = new ArrayList<>(imgPaths); + Stamp stamp = this.convertStampImg(stampRequestDto, imgList, userId, missionId); + + //๋žญํฌ ๊ด€๋ จ ์ ์ˆ˜ ์ฒ˜๋ฆฌ + User user = userRepositoryV1.findUserById(Long.valueOf(userId)) + .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); + + //๋ฏธ์…˜ ๋žญํฌ์ ์ˆ˜ ์•Œ์•„์˜ค๊ธฐ + Mission mission = missionRepositoryV1.findById(missionId) + .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); + + user.addPoints(mission.getLevel()); + userRepositoryV1.save(user); + + return stampRepositoryV1.save(stamp); + } + + //์‚ฌ์ง„ ์ˆ˜์ • ํ•  ๊ฒฝ์šฐ + @Transactional + public Stamp editStampWithImg(StampRequestDto stampRequestDto, List imgPaths, + String userId, + Long missionId) { + + Stamp stamp = stampRepositoryV1.findByUserIdAndMissionId(Long.valueOf(userId), missionId); + + if (StringUtils.hasText(stampRequestDto.getContents())) { + stamp.changeContents(stampRequestDto.getContents()); + } + stamp.changeImages(imgPaths); + stamp.setUpdatedAt(LocalDateTime.now()); + + return stampRepositoryV1.save(stamp); + } + + + //์‚ฌ์ง„ ์ˆ˜์ • ์•ˆํ•  ๊ฒฝ์šฐ + @Transactional + public Stamp editStampContents(StampRequestDto stampRequestDto, String userId, + Long missionId) { + + Stamp stamp = stampRepositoryV1.findByUserIdAndMissionId(Long.valueOf(userId), missionId); + + if (StringUtils.hasText(stampRequestDto.getContents())) { + stamp.changeContents(stampRequestDto.getContents()); + } + + stamp.setUpdatedAt(LocalDateTime.now()); + return stampRepositoryV1.save(stamp); + } + + + //Stamp ์‚ญ์ œ by stampId + @Transactional + public void deleteByStampId(Long stampId) { + + Stamp stamp = stampRepositoryV1.findById(stampId) + .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); + + //๋žญํฌ ๊ด€๋ จ ์ ์ˆ˜ ์ฒ˜๋ฆฌ + User user = userRepositoryV1.findUserById(stamp.getUserId()) + .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); + + //๋ฏธ์…˜ ๋žญํฌ์ ์ˆ˜ ์•Œ์•„์˜ค๊ธฐ + Mission mission = missionRepositoryV1.findById(stamp.getMissionId()) + .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); + + user.minusPoints(mission.getLevel()); + userRepositoryV1.save(user); + + stampRepositoryV1.deleteById(stampId); + } + + + //์Šคํƒฌํ”„ ์ค‘๋ณต ๊ฒ€์‚ฌ์ฒดํฌ + @Transactional + public void checkDuplicateStamp(String userId, Long missionId) { + Stamp stamp = stampRepositoryV1.findByUserIdAndMissionId(Long.valueOf(userId), missionId); + + if (stamp != null) { + throw new ApiException(DUPLICATE_STAMP); + } + } + + + //Stamp ์‚ญ์ œ All by UserId + @Transactional + public void deleteStampByUserId(Long userId) { + + //์Šคํƒฌํ”„ ์ „๋ถ€์‚ญ์ œ + stampRepositoryV1.deleteAllByUserId(userId); + + //ํ•ด๋‹น ์Šคํƒฌํ”„๋กœ ์–ป์—ˆ๋˜ ์ ์ˆ˜ ๋ชจ๋‘ ์ดˆ๊ธฐํ™” + User user = userRepositoryV1.findUserById(userId) + .orElseThrow(() -> new ApiException(INVALID_RESPONSE)); + user.initializePoints(); + userRepositoryV1.save(user); + + } + + + //Stamp Entity ์–‘์‹์— ๋งž๊ฒŒ ๋ฐ์ดํ„ฐ ์„ธํŒ… + private Stamp convertStampImg(StampRequestDto stampRequestDto, List imgList, + String userId, Long missionId) { + return Stamp.builder() + .contents(stampRequestDto.getContents()) + .createdAt(LocalDateTime.now()) + .images(imgList) + .missionId(missionId) + .userId(Long.valueOf(userId)) + .build(); + } + +} diff --git a/src/main/java/org/sopt/app/v1/application/user/UserUseCase.java b/src/main/java/org/sopt/app/v1/application/user/UserUseCase.java new file mode 100644 index 00000000..72c2beec --- /dev/null +++ b/src/main/java/org/sopt/app/v1/application/user/UserUseCase.java @@ -0,0 +1,12 @@ +package org.sopt.app.v1.application.user; + +import org.sopt.app.domain.entity.User; +import org.sopt.app.v1.application.user.dto.LogInUserDto; +import org.sopt.app.v1.application.user.dto.SignUpUserDto; + +public interface UserUseCase { + + Long signUp(SignUpUserDto userDto); + + User logIn(LogInUserDto userDto); +} diff --git a/src/main/java/org/sopt/app/v1/application/user/UserUseCaseImpl.java b/src/main/java/org/sopt/app/v1/application/user/UserUseCaseImpl.java new file mode 100644 index 00000000..43bbd419 --- /dev/null +++ b/src/main/java/org/sopt/app/v1/application/user/UserUseCaseImpl.java @@ -0,0 +1,36 @@ +package org.sopt.app.v1.application.user; + +import lombok.RequiredArgsConstructor; +import org.sopt.app.common.exception.v1.UserNotFoundException; +import org.sopt.app.domain.entity.User; +import org.sopt.app.v1.application.auth.EncryptService; +import org.sopt.app.v1.application.user.dto.LogInUserDto; +import org.sopt.app.v1.application.user.dto.SignUpUserDto; +import org.sopt.app.v1.application.user.service.UserServiceV1; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserUseCaseImpl implements UserUseCase { + + private final UserServiceV1 userServiceV1; + private final EncryptService encryptService; + + @Override + public Long signUp(SignUpUserDto userDto) { + String encodedPassword = encryptService.encode(userDto.getPassword()); + return userServiceV1.create(userDto, encodedPassword); + } + + @Override + public User logIn(LogInUserDto userDto) { + User user = userServiceV1.findUserByEmail(userDto.email()); + + boolean matches = encryptService.matches(userDto.password(), user.getPassword()); + if (!matches) { + throw new UserNotFoundException("์•„์ด๋””/๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”."); + } + + return user; + } +} diff --git a/src/main/java/org/sopt/app/application/user/dto/LogInUserDto.java b/src/main/java/org/sopt/app/v1/application/user/dto/LogInUserDto.java similarity index 74% rename from src/main/java/org/sopt/app/application/user/dto/LogInUserDto.java rename to src/main/java/org/sopt/app/v1/application/user/dto/LogInUserDto.java index 9ed97152..bcd6d5e3 100644 --- a/src/main/java/org/sopt/app/application/user/dto/LogInUserDto.java +++ b/src/main/java/org/sopt/app/v1/application/user/dto/LogInUserDto.java @@ -1,4 +1,4 @@ -package org.sopt.app.application.user.dto; +package org.sopt.app.v1.application.user.dto; import lombok.Builder; diff --git a/src/main/java/org/sopt/app/application/user/dto/SignUpUserDto.java b/src/main/java/org/sopt/app/v1/application/user/dto/SignUpUserDto.java similarity index 92% rename from src/main/java/org/sopt/app/application/user/dto/SignUpUserDto.java rename to src/main/java/org/sopt/app/v1/application/user/dto/SignUpUserDto.java index f41d60d6..25526f7d 100644 --- a/src/main/java/org/sopt/app/application/user/dto/SignUpUserDto.java +++ b/src/main/java/org/sopt/app/v1/application/user/dto/SignUpUserDto.java @@ -1,4 +1,4 @@ -package org.sopt.app.application.user.dto; +package org.sopt.app.v1.application.user.dto; import lombok.Builder; import lombok.Getter; @@ -6,6 +6,7 @@ @Getter public class SignUpUserDto { + private final String nickname; private final String email; private final String password; diff --git a/src/main/java/org/sopt/app/v1/application/user/service/UserServiceV1.java b/src/main/java/org/sopt/app/v1/application/user/service/UserServiceV1.java new file mode 100644 index 00000000..9b50ca9a --- /dev/null +++ b/src/main/java/org/sopt/app/v1/application/user/service/UserServiceV1.java @@ -0,0 +1,34 @@ +package org.sopt.app.v1.application.user.service; + +import lombok.RequiredArgsConstructor; +import org.sopt.app.common.exception.v1.UserNotFoundException; +import org.sopt.app.domain.entity.User; +import org.sopt.app.v1.application.user.dto.SignUpUserDto; +import org.sopt.app.v1.interfaces.postgres.UserRepositoryV1; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserServiceV1 { + + private final UserRepositoryV1 userRepositoryV1; + + public Long create(SignUpUserDto userDto, String password) { + User user = userRepositoryV1.save(User.builder() + .username("") + .nickname(userDto.getNickname()) + .email(userDto.getEmail()) + .password(password) + .osType(null) + .points(0L) + .clientToken(userDto.getClientToken()) + .build()); + + return user.getId(); + } + + public User findUserByEmail(String email) throws UserNotFoundException { + return userRepositoryV1.findUserByEmail(email) + .orElseThrow(() -> new UserNotFoundException("์•„์ด๋””/๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.")); + } +} diff --git a/src/main/java/org/sopt/app/v1/interfaces/postgres/MissionRepositoryV1.java b/src/main/java/org/sopt/app/v1/interfaces/postgres/MissionRepositoryV1.java new file mode 100644 index 00000000..e4f8add5 --- /dev/null +++ b/src/main/java/org/sopt/app/v1/interfaces/postgres/MissionRepositoryV1.java @@ -0,0 +1,14 @@ +package org.sopt.app.v1.interfaces.postgres; + +import java.util.List; +import org.sopt.app.domain.entity.Mission; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface MissionRepositoryV1 extends JpaRepository { + + @Query("select m from Mission m where m.id in :missions") + List findMissionIn(@Param("missions") List missions); + +} diff --git a/src/main/java/org/sopt/app/v1/interfaces/postgres/StampRepositoryV1.java b/src/main/java/org/sopt/app/v1/interfaces/postgres/StampRepositoryV1.java new file mode 100644 index 00000000..27c35a54 --- /dev/null +++ b/src/main/java/org/sopt/app/v1/interfaces/postgres/StampRepositoryV1.java @@ -0,0 +1,15 @@ +package org.sopt.app.v1.interfaces.postgres; + +import java.util.List; +import org.sopt.app.domain.entity.Stamp; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StampRepositoryV1 extends JpaRepository { + + List findAllByUserId(Long userId); + + Stamp findByUserIdAndMissionId(Long userId, Long missionId); + + void deleteAllByUserId(Long userId); + +} diff --git a/src/main/java/org/sopt/app/v1/interfaces/postgres/UserRepositoryV1.java b/src/main/java/org/sopt/app/v1/interfaces/postgres/UserRepositoryV1.java new file mode 100644 index 00000000..95061450 --- /dev/null +++ b/src/main/java/org/sopt/app/v1/interfaces/postgres/UserRepositoryV1.java @@ -0,0 +1,14 @@ +package org.sopt.app.v1.interfaces.postgres; + +import java.util.Optional; +import org.sopt.app.domain.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepositoryV1 extends JpaRepository { + + Optional findUserByEmail(String email); + + Optional findUserByNickname(String nickname); + + Optional findUserById(Long userId); +} diff --git a/src/main/java/org/sopt/app/presentation/BaseController.java b/src/main/java/org/sopt/app/v1/presentation/BaseController.java similarity index 94% rename from src/main/java/org/sopt/app/presentation/BaseController.java rename to src/main/java/org/sopt/app/v1/presentation/BaseController.java index 4edf9cef..e1420082 100644 --- a/src/main/java/org/sopt/app/presentation/BaseController.java +++ b/src/main/java/org/sopt/app/v1/presentation/BaseController.java @@ -1,18 +1,19 @@ -package org.sopt.app.presentation; +package org.sopt.app.v1.presentation; +import static org.sopt.app.common.ResponseCode.SUCCESS; +import static org.sopt.app.common.constants.Constants.RESULT_CODE; + +import java.nio.charset.StandardCharsets; import lombok.AllArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; -import java.nio.charset.StandardCharsets; -import static org.sopt.app.common.ResponseCode.SUCCESS; -import static org.sopt.app.common.constants.Constants.RESULT_CODE; - @Controller @AllArgsConstructor public class BaseController { + protected HttpHeaders getSuccessHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(new MediaType("application", "json", StandardCharsets.UTF_8)); diff --git a/src/main/java/org/sopt/app/v1/presentation/auth/AuthControllerV1.java b/src/main/java/org/sopt/app/v1/presentation/auth/AuthControllerV1.java new file mode 100644 index 00000000..136a38d1 --- /dev/null +++ b/src/main/java/org/sopt/app/v1/presentation/auth/AuthControllerV1.java @@ -0,0 +1,64 @@ +package org.sopt.app.v1.presentation.auth; + + +import lombok.RequiredArgsConstructor; +import org.sopt.app.v1.application.auth.AuthUseCaseImpl; +import org.sopt.app.v1.presentation.auth.dto.ChangeNicknameRequestDto; +import org.sopt.app.v1.presentation.auth.dto.ChangePasswordRequestDto; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class AuthControllerV1 { + + private final AuthUseCaseImpl authUseCase; + + /** + * ๋‹‰๋„ค์ž„, ์ด๋ฉ”์ผ ๊ฒ€์ฆ API + */ + @GetMapping(value = "/api/v1/auth") + public void check(@RequestParam(value = "nickname", required = false) String nickname, + @RequestParam(value = "email", required = false) String email) { + authUseCase.validate(nickname, email); + } + + /** + * ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ + */ + + @PatchMapping(value = "/api/v1/auth/password") + public void changePassword( + @RequestHeader(name = "userId") String userId, + @RequestBody ChangePasswordRequestDto changePasswordRequestDto + ) { + authUseCase.changePassword(userId, changePasswordRequestDto.getPassword()); + } + + /** + * ๋‹‰๋„ค์ž„ ๋ณ€๊ฒฝ + */ + @PatchMapping(value = "/api/v1/auth/nickname") + public void changeNickname( + @RequestHeader(name = "userId") String userId, + @RequestBody ChangeNicknameRequestDto changeNicknameRequestDto + ) { + String nickname = changeNicknameRequestDto.getNickname(); + authUseCase.changeNickname(userId, nickname); + } + + /** + * ํƒˆํ‡ดํ•˜๊ธฐ + */ + @DeleteMapping(value = "/api/v1/auth/withdraw") + public void withdraw( + @RequestHeader(name = "userId") String userId + ) { + authUseCase.deleteUser(userId); + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/app/presentation/auth/dto/ChangeNicknameRequestDto.java b/src/main/java/org/sopt/app/v1/presentation/auth/dto/ChangeNicknameRequestDto.java similarity index 83% rename from src/main/java/org/sopt/app/presentation/auth/dto/ChangeNicknameRequestDto.java rename to src/main/java/org/sopt/app/v1/presentation/auth/dto/ChangeNicknameRequestDto.java index 5f44db8c..1630088f 100644 --- a/src/main/java/org/sopt/app/presentation/auth/dto/ChangeNicknameRequestDto.java +++ b/src/main/java/org/sopt/app/v1/presentation/auth/dto/ChangeNicknameRequestDto.java @@ -1,4 +1,4 @@ -package org.sopt.app.presentation.auth.dto; +package org.sopt.app.v1.presentation.auth.dto; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/sopt/app/presentation/auth/dto/ChangePasswordRequestDto.java b/src/main/java/org/sopt/app/v1/presentation/auth/dto/ChangePasswordRequestDto.java similarity index 83% rename from src/main/java/org/sopt/app/presentation/auth/dto/ChangePasswordRequestDto.java rename to src/main/java/org/sopt/app/v1/presentation/auth/dto/ChangePasswordRequestDto.java index a037dc3c..ee796a70 100644 --- a/src/main/java/org/sopt/app/presentation/auth/dto/ChangePasswordRequestDto.java +++ b/src/main/java/org/sopt/app/v1/presentation/auth/dto/ChangePasswordRequestDto.java @@ -1,4 +1,4 @@ -package org.sopt.app.presentation.auth.dto; +package org.sopt.app.v1.presentation.auth.dto; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/sopt/app/v1/presentation/firebase/FirebaseControllerV1.java b/src/main/java/org/sopt/app/v1/presentation/firebase/FirebaseControllerV1.java new file mode 100644 index 00000000..4c0027aa --- /dev/null +++ b/src/main/java/org/sopt/app/v1/presentation/firebase/FirebaseControllerV1.java @@ -0,0 +1,31 @@ +package org.sopt.app.v1.presentation.firebase; + + +import lombok.RequiredArgsConstructor; +import org.sopt.app.v1.presentation.firebase.dto.FirebaseResponseDto; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class FirebaseControllerV1 { + + + /** + * firebase ์—ฐ๋™์„ ์œ„ํ•œ ์ •๋ณด GET + * + * @return + */ + @GetMapping(value = "/api/v1/firebase") + public FirebaseResponseDto getfirebaseInfo() { + + return FirebaseResponseDto.builder() + .iosForceUpdateVersion("1.0.0") + .iosAppVersion("1.0.2") + .androidForceUpdateVersion("1.0.0") + .androidAppVersion("1.0.0") + .notice("์•ˆ๋…•ํ•˜์„ธ์š”, makers์ž…๋‹ˆ๋‹ค. \n ํ˜„์žฌ ๋ฏธ์…˜ ์ˆ˜์ •/๋“ฑ๋ก์ด ๋ถˆ๊ฐ€๋Šฅํ•œ ์ด์Šˆ๊ฐ€ ํ™•์ธ๋˜์–ด ์›์ธ ํŒŒ์•… ์ค‘์— ์žˆ์Šต๋‹ˆ๋‹ค. \n ์•ฑ ์ด์šฉ์— ๋ถˆํŽธ์„ ๋“œ๋ฆฐ ์  ์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. \n ๋น ๋ฅธ ์‹œ์ผ ๋‚ด ๋ณต๊ตฌ ํ›„ ์žฌ๊ณต์ง€ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.") + .imgUrl(null) + .build(); + } +} diff --git a/src/main/java/org/sopt/app/presentation/firebase/dto/FirebaseResponseDto.java b/src/main/java/org/sopt/app/v1/presentation/firebase/dto/FirebaseResponseDto.java similarity index 92% rename from src/main/java/org/sopt/app/presentation/firebase/dto/FirebaseResponseDto.java rename to src/main/java/org/sopt/app/v1/presentation/firebase/dto/FirebaseResponseDto.java index 70fe74c0..af7e00b1 100644 --- a/src/main/java/org/sopt/app/presentation/firebase/dto/FirebaseResponseDto.java +++ b/src/main/java/org/sopt/app/v1/presentation/firebase/dto/FirebaseResponseDto.java @@ -1,4 +1,4 @@ -package org.sopt.app.presentation.firebase.dto; +package org.sopt.app.v1.presentation.firebase.dto; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/sopt/app/v1/presentation/mission/MissionControllerV1.java b/src/main/java/org/sopt/app/v1/presentation/mission/MissionControllerV1.java new file mode 100644 index 00000000..2f920b57 --- /dev/null +++ b/src/main/java/org/sopt/app/v1/presentation/mission/MissionControllerV1.java @@ -0,0 +1,95 @@ +package org.sopt.app.v1.presentation.mission; + + +import java.util.List; +import lombok.AllArgsConstructor; +import org.sopt.app.application.s3.S3Service; +import org.sopt.app.domain.entity.Mission; +import org.sopt.app.v1.application.mission.MissionServiceV1; +import org.sopt.app.v1.presentation.BaseController; +import org.sopt.app.v1.presentation.mission.dto.MissionRequestDto; +import org.sopt.app.v1.presentation.mission.dto.MissionResponseDto; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/v1/mission") +public class MissionControllerV1 extends BaseController { + + private final MissionServiceV1 missionServiceV1; + private final S3Service s3Service; + + + /** + * ์ „์ฒด mission ์กฐํšŒํ•˜๊ธฐ + */ + @GetMapping(value = "/all") + @ResponseBody + public ResponseEntity findAllMission(@RequestHeader("userId") String userId) { + return new ResponseEntity<>(missionServiceV1.findAllMission(userId), getSuccessHeaders(), + HttpStatus.OK); + } + + + /** + * ๋ฏธ์…˜ ์—…๋กœ๋“œ ํ•˜๊ธฐ + */ + @PostMapping() + public ResponseEntity uploadMission( + @RequestPart("missionContent") MissionRequestDto missionRequestDto, + @RequestPart(name = "imgUrl", required = false) List multipartFiles) { + + //MultipartFile์„ ๋ฆฌ์ŠคํŠธ์— ๋„ฃ์–ด์คฌ๊ธฐ ๋•Œ๋ฌธ์— List ๋‚ด๋ถ€์˜ ์ด๋ฏธ์ง€ํŒŒ์ผ์— isEmpty()๋ฅผ ์ ์šฉํ•ด์•ผ ํ•œ๋‹ค. + int checkNum = 1; + for (MultipartFile image : multipartFiles) { + if (image.isEmpty()) { + checkNum = 0; + } + } + + MissionResponseDto result = MissionResponseDto.builder().build(); + if (checkNum == 0) { + Mission mission = missionServiceV1.uploadMission(missionRequestDto); + result.setMissionId(mission.getId()); + } else { + List imgPaths = s3Service.uploadDeprecated(multipartFiles); + Mission uploadMissionWithImg = missionServiceV1.uploadMissionWithImg(missionRequestDto, + imgPaths); + result.setMissionId(uploadMissionWithImg.getId()); + } + + return new ResponseEntity<>(result, getSuccessHeaders(), HttpStatus.OK); + } + + + /** + * ์™„๋ฃŒ ๋ฏธ์…˜๋งŒ ์กฐํšŒํ•˜๊ธฐ + */ + @GetMapping("complete") + public ResponseEntity findCompleteMission(@RequestHeader("userId") String userId) { + + List resultMission = missionServiceV1.getCompleteMission(userId); + + return new ResponseEntity<>(resultMission, getSuccessHeaders(), HttpStatus.OK); + } + + /** + * ๋ฏธ์™„๋ฃŒ ๋ฏธ์…˜๋งŒ ์กฐํšŒํ•˜๊ธฐ + */ + @GetMapping("incomplete") + public ResponseEntity findInCompleteMission(@RequestHeader("userId") String userId) { + List resultMission = missionServiceV1.getIncompleteMission(userId); + + return new ResponseEntity<>(resultMission, getSuccessHeaders(), HttpStatus.OK); + } + +} diff --git a/src/main/java/org/sopt/app/presentation/mission/dto/GetAllMissionResponseDto.java b/src/main/java/org/sopt/app/v1/presentation/mission/dto/GetAllMissionResponseDto.java similarity index 79% rename from src/main/java/org/sopt/app/presentation/mission/dto/GetAllMissionResponseDto.java rename to src/main/java/org/sopt/app/v1/presentation/mission/dto/GetAllMissionResponseDto.java index 6c0b2b24..d0d46948 100644 --- a/src/main/java/org/sopt/app/presentation/mission/dto/GetAllMissionResponseDto.java +++ b/src/main/java/org/sopt/app/v1/presentation/mission/dto/GetAllMissionResponseDto.java @@ -1,13 +1,16 @@ -package org.sopt.app.presentation.mission.dto; - -import lombok.*; +package org.sopt.app.v1.presentation.mission.dto; import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @Getter @Setter @NoArgsConstructor public class GetAllMissionResponseDto { + private Long id; private String title; private Integer level; diff --git a/src/main/java/org/sopt/app/presentation/mission/dto/MissionRequestDto.java b/src/main/java/org/sopt/app/v1/presentation/mission/dto/MissionRequestDto.java similarity index 85% rename from src/main/java/org/sopt/app/presentation/mission/dto/MissionRequestDto.java rename to src/main/java/org/sopt/app/v1/presentation/mission/dto/MissionRequestDto.java index 80d64139..8952d5ff 100644 --- a/src/main/java/org/sopt/app/presentation/mission/dto/MissionRequestDto.java +++ b/src/main/java/org/sopt/app/v1/presentation/mission/dto/MissionRequestDto.java @@ -1,4 +1,4 @@ -package org.sopt.app.presentation.mission.dto; +package org.sopt.app.v1.presentation.mission.dto; import lombok.AllArgsConstructor; diff --git a/src/main/java/org/sopt/app/presentation/mission/dto/MissionResponseDto.java b/src/main/java/org/sopt/app/v1/presentation/mission/dto/MissionResponseDto.java similarity index 84% rename from src/main/java/org/sopt/app/presentation/mission/dto/MissionResponseDto.java rename to src/main/java/org/sopt/app/v1/presentation/mission/dto/MissionResponseDto.java index c8e6efcc..4e946c51 100644 --- a/src/main/java/org/sopt/app/presentation/mission/dto/MissionResponseDto.java +++ b/src/main/java/org/sopt/app/v1/presentation/mission/dto/MissionResponseDto.java @@ -1,4 +1,4 @@ -package org.sopt.app.presentation.mission.dto; +package org.sopt.app.v1.presentation.mission.dto; import lombok.AllArgsConstructor; diff --git a/src/main/java/org/sopt/app/v1/presentation/rank/RankControllerV1.java b/src/main/java/org/sopt/app/v1/presentation/rank/RankControllerV1.java new file mode 100644 index 00000000..38933171 --- /dev/null +++ b/src/main/java/org/sopt/app/v1/presentation/rank/RankControllerV1.java @@ -0,0 +1,48 @@ +package org.sopt.app.v1.presentation.rank; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.sopt.app.domain.entity.User; +import org.sopt.app.v1.application.rank.RankServiceV1; +import org.sopt.app.v1.presentation.BaseController; +import org.sopt.app.v1.presentation.rank.dto.FindAllRanksResponseDto; +import org.sopt.app.v1.presentation.rank.dto.FindRankResponseDto; +import org.sopt.app.v1.presentation.rank.dto.UserProfileRequestDto; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/rank") +public class RankControllerV1 extends BaseController { + + private final RankServiceV1 rankServiceV1; + + /** + * ํ•œ๋งˆ๋”” ํŽธ์ง‘ํ•˜๊ธฐ + */ + @PostMapping("/profileMessage") + public User updateUserProfileMessage( + @RequestHeader Long userId, + @RequestBody UserProfileRequestDto userProfileRequestDto + ) { + + return rankServiceV1 + .updateProfileMessage(userId, userProfileRequestDto.getProfileMessage()); + } + + @GetMapping("") + public List findRanks() { + return rankServiceV1.findRanks(); + } + + @GetMapping("/detail") + public FindRankResponseDto findRankById( + @RequestHeader Long userId) { + return rankServiceV1.findRankById(userId); + } +} diff --git a/src/main/java/org/sopt/app/presentation/rank/dto/FindAllRanksResponseDto.java b/src/main/java/org/sopt/app/v1/presentation/rank/dto/FindAllRanksResponseDto.java similarity index 88% rename from src/main/java/org/sopt/app/presentation/rank/dto/FindAllRanksResponseDto.java rename to src/main/java/org/sopt/app/v1/presentation/rank/dto/FindAllRanksResponseDto.java index a93fd3e4..4af1bbb1 100644 --- a/src/main/java/org/sopt/app/presentation/rank/dto/FindAllRanksResponseDto.java +++ b/src/main/java/org/sopt/app/v1/presentation/rank/dto/FindAllRanksResponseDto.java @@ -1,4 +1,4 @@ -package org.sopt.app.presentation.rank.dto; +package org.sopt.app.v1.presentation.rank.dto; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/sopt/app/presentation/rank/dto/FindRankResponseDto.java b/src/main/java/org/sopt/app/v1/presentation/rank/dto/FindRankResponseDto.java similarity index 89% rename from src/main/java/org/sopt/app/presentation/rank/dto/FindRankResponseDto.java rename to src/main/java/org/sopt/app/v1/presentation/rank/dto/FindRankResponseDto.java index ce419e48..c1df87c9 100644 --- a/src/main/java/org/sopt/app/presentation/rank/dto/FindRankResponseDto.java +++ b/src/main/java/org/sopt/app/v1/presentation/rank/dto/FindRankResponseDto.java @@ -1,18 +1,18 @@ -package org.sopt.app.presentation.rank.dto; +package org.sopt.app.v1.presentation.rank.dto; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.sopt.app.domain.entity.Mission; -import java.util.List; - @Getter @Builder @AllArgsConstructor @NoArgsConstructor public class FindRankResponseDto { + private Long userId; private String nickname; private String profileMessage; diff --git a/src/main/java/org/sopt/app/presentation/rank/dto/UserProfileRequestDto.java b/src/main/java/org/sopt/app/v1/presentation/rank/dto/UserProfileRequestDto.java similarity index 81% rename from src/main/java/org/sopt/app/presentation/rank/dto/UserProfileRequestDto.java rename to src/main/java/org/sopt/app/v1/presentation/rank/dto/UserProfileRequestDto.java index 11dad837..942d0fc7 100644 --- a/src/main/java/org/sopt/app/presentation/rank/dto/UserProfileRequestDto.java +++ b/src/main/java/org/sopt/app/v1/presentation/rank/dto/UserProfileRequestDto.java @@ -1,4 +1,4 @@ -package org.sopt.app.presentation.rank.dto; +package org.sopt.app.v1.presentation.rank.dto; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/org/sopt/app/v1/presentation/stamp/StampControllerV1.java b/src/main/java/org/sopt/app/v1/presentation/stamp/StampControllerV1.java new file mode 100644 index 00000000..d7c17d19 --- /dev/null +++ b/src/main/java/org/sopt/app/v1/presentation/stamp/StampControllerV1.java @@ -0,0 +1,117 @@ +package org.sopt.app.v1.presentation.stamp; + +import java.util.List; +import lombok.AllArgsConstructor; +import org.sopt.app.application.s3.S3Service; +import org.sopt.app.domain.entity.Stamp; +import org.sopt.app.v1.application.stamp.StampServiceV1; +import org.sopt.app.v1.presentation.BaseController; +import org.sopt.app.v1.presentation.stamp.dto.StampRequestDto; +import org.sopt.app.v1.presentation.stamp.dto.StampResponseDto; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/v1/stamp") +public class StampControllerV1 extends BaseController { + + private final StampServiceV1 stampServiceV1; + + private final S3Service s3Service; + + + /** + * stamp ์กฐํšŒ missionId, userId๋กœ stamp ์กฐํšŒํ•˜๊ธฐ + */ + @GetMapping("/{missionId}") + public Stamp findStampByMissionAndUserId( + @RequestHeader("userId") String userId, + @PathVariable Long missionId) { + + return stampServiceV1.findStamp(userId, missionId); + } + + /** + * ๋ฏธ์…˜ ๋‹ฌ์„ฑ์„ ์œ„ํ•ด ๋‚ด์šฉ์„ Stamp ๋‚ด์šฉ ๋“ฑ๋ก + */ + @PostMapping("/{missionId}") + public ResponseEntity uploadStamp( + @RequestHeader("userId") String userId, + @PathVariable Long missionId, + @RequestPart("stampContent") StampRequestDto stampRequestDto, + @RequestPart(name = "imgUrl", required = false) List multipartFiles + ) { + + //์Šคํƒฌํ”„ ์ค‘๋ณต ๊ฒ€์‚ฌ์ฒดํฌ + stampServiceV1.checkDuplicateStamp(userId, missionId); + + List imgPaths = s3Service.uploadDeprecated(multipartFiles); + Stamp uploadStamp = stampServiceV1.uploadStamp(stampRequestDto, + imgPaths, userId, missionId); + + return new ResponseEntity<>(uploadStamp, getSuccessHeaders(), HttpStatus.OK); + } + + + @PutMapping("/{missionId}") + public ResponseEntity editStamp( + @RequestHeader("userId") String userId, + @PathVariable Long missionId, + @RequestPart(value = "stampContent", required = false) StampRequestDto stampRequestDto, + @RequestPart(name = "imgUrl", required = false) List multipartFiles + + ) { + //MultipartFile์„ ๋ฆฌ์ŠคํŠธ์— ๋„ฃ์–ด์คฌ๊ธฐ ๋•Œ๋ฌธ์— List ๋‚ด๋ถ€์˜ ์ด๋ฏธ์ง€ํŒŒ์ผ์— isEmpty()๋ฅผ ์ ์šฉํ•ด์•ผ ํ•œ๋‹ค. + int checkNum = 1; + for (MultipartFile image : multipartFiles) { + if (image.isEmpty()) { + checkNum = 0; + } + } + + StampResponseDto result = StampResponseDto.builder().build(); + if (checkNum == 0) { + + Stamp stamp = stampServiceV1.editStampContents(stampRequestDto, userId, missionId); + result.setStampId(stamp.getId()); + + } else { + + List imgPaths = s3Service.uploadDeprecated(multipartFiles); + Stamp uploadStamp = stampServiceV1.editStampWithImg(stampRequestDto, + imgPaths, userId, missionId); + + result.setStampId(uploadStamp.getId()); + } + return new ResponseEntity<>(result, getSuccessHeaders(), HttpStatus.OK); + } + + /** + * Stamp ๊ฐœ๋ณ„ ์‚ญ์ œ + */ + @DeleteMapping("/{stampId}") + public ResponseEntity deleteStampById(@PathVariable Long stampId) { + stampServiceV1.deleteByStampId(stampId); + return new ResponseEntity<>("{}", getSuccessHeaders(), HttpStatus.OK); + } + + /** + * ์ „์ฒด Stamp์‚ญ์ œ + */ + @DeleteMapping("/all") + public ResponseEntity deleteStampByUserId(@RequestHeader Long userId) { + stampServiceV1.deleteStampByUserId(userId); + return new ResponseEntity<>("{}", getSuccessHeaders(), HttpStatus.OK); + } +} diff --git a/src/main/java/org/sopt/app/presentation/stamp/dto/StampRequestDto.java b/src/main/java/org/sopt/app/v1/presentation/stamp/dto/StampRequestDto.java similarity index 83% rename from src/main/java/org/sopt/app/presentation/stamp/dto/StampRequestDto.java rename to src/main/java/org/sopt/app/v1/presentation/stamp/dto/StampRequestDto.java index 5bdfb00a..8ef7adef 100644 --- a/src/main/java/org/sopt/app/presentation/stamp/dto/StampRequestDto.java +++ b/src/main/java/org/sopt/app/v1/presentation/stamp/dto/StampRequestDto.java @@ -1,4 +1,4 @@ -package org.sopt.app.presentation.stamp.dto; +package org.sopt.app.v1.presentation.stamp.dto; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/sopt/app/presentation/stamp/dto/StampResponseDto.java b/src/main/java/org/sopt/app/v1/presentation/stamp/dto/StampResponseDto.java similarity index 84% rename from src/main/java/org/sopt/app/presentation/stamp/dto/StampResponseDto.java rename to src/main/java/org/sopt/app/v1/presentation/stamp/dto/StampResponseDto.java index bb23ce50..57fae49b 100644 --- a/src/main/java/org/sopt/app/presentation/stamp/dto/StampResponseDto.java +++ b/src/main/java/org/sopt/app/v1/presentation/stamp/dto/StampResponseDto.java @@ -1,4 +1,4 @@ -package org.sopt.app.presentation.stamp.dto; +package org.sopt.app.v1.presentation.stamp.dto; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/org/sopt/app/v1/presentation/user/UserControllerV1.java b/src/main/java/org/sopt/app/v1/presentation/user/UserControllerV1.java new file mode 100644 index 00000000..f61f18c2 --- /dev/null +++ b/src/main/java/org/sopt/app/v1/presentation/user/UserControllerV1.java @@ -0,0 +1,56 @@ +package org.sopt.app.v1.presentation.user; + +import javax.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.sopt.app.domain.entity.User; +import org.sopt.app.v1.application.user.UserUseCase; +import org.sopt.app.v1.application.user.dto.LogInUserDto; +import org.sopt.app.v1.application.user.dto.SignUpUserDto; +import org.sopt.app.v1.presentation.user.request.LogInUserRequest; +import org.sopt.app.v1.presentation.user.response.LoginResponse; +import org.sopt.app.v1.presentation.user.response.SignUpResponse; +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 UserControllerV1 { + + private final UserUseCase userUseCase; + + /** + * ์ด๋ฉ”์ผ ํšŒ์›๊ฐ€์ž… + */ + @PostMapping(value = "/api/v1/user/signup") + public SignUpResponse signUp2(@RequestBody SignUpUserDto request) { + Long userId = userUseCase.signUp(SignUpUserDto.builder() + .nickname(request.getNickname()) + .email(request.getEmail()) + .password(request.getPassword()) + .osType(request.getOsType()) + .clientToken(request.getClientToken()) + .build()); + + return SignUpResponse.builder() + .userId(userId) + .build(); + } + + + /** + * ๋กœ๊ทธ์ธ + */ + @PostMapping(value = "/api/v1/user/login") + public LoginResponse signIn2(@Valid @RequestBody LogInUserRequest request) { + + User user = userUseCase.logIn(LogInUserDto.builder() + .email(request.getEmail()) + .password(request.getPassword()) + .build()); + return LoginResponse.builder() + .userId(user.getId()) + .profileMessage(user.getProfileMessage()) + .build(); + } +} diff --git a/src/main/java/org/sopt/app/presentation/user/request/CreateUserRequest.java b/src/main/java/org/sopt/app/v1/presentation/user/request/CreateUserRequest.java similarity index 86% rename from src/main/java/org/sopt/app/presentation/user/request/CreateUserRequest.java rename to src/main/java/org/sopt/app/v1/presentation/user/request/CreateUserRequest.java index 90e7b288..099ba336 100644 --- a/src/main/java/org/sopt/app/presentation/user/request/CreateUserRequest.java +++ b/src/main/java/org/sopt/app/v1/presentation/user/request/CreateUserRequest.java @@ -1,4 +1,4 @@ -package org.sopt.app.presentation.user.request; +package org.sopt.app.v1.presentation.user.request; import com.sun.istack.NotNull; import lombok.Getter; @@ -6,6 +6,7 @@ @Getter public class CreateUserRequest { + @NotNull private String nickname; @NotNull diff --git a/src/main/java/org/sopt/app/presentation/user/request/LogInUserRequest.java b/src/main/java/org/sopt/app/v1/presentation/user/request/LogInUserRequest.java similarity index 69% rename from src/main/java/org/sopt/app/presentation/user/request/LogInUserRequest.java rename to src/main/java/org/sopt/app/v1/presentation/user/request/LogInUserRequest.java index c944aaba..fb6af1c7 100644 --- a/src/main/java/org/sopt/app/presentation/user/request/LogInUserRequest.java +++ b/src/main/java/org/sopt/app/v1/presentation/user/request/LogInUserRequest.java @@ -1,14 +1,17 @@ -package org.sopt.app.presentation.user.request; - -import lombok.*; +package org.sopt.app.v1.presentation.user.request; import javax.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class LogInUserRequest { + @NotBlank(message = "์•„์ด๋””๋‚˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") private String email; @NotBlank(message = "์•„์ด๋””๋‚˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") diff --git a/src/main/java/org/sopt/app/presentation/user/response/LoginResponse.java b/src/main/java/org/sopt/app/v1/presentation/user/response/LoginResponse.java similarity index 85% rename from src/main/java/org/sopt/app/presentation/user/response/LoginResponse.java rename to src/main/java/org/sopt/app/v1/presentation/user/response/LoginResponse.java index f0283038..2ffb719d 100644 --- a/src/main/java/org/sopt/app/presentation/user/response/LoginResponse.java +++ b/src/main/java/org/sopt/app/v1/presentation/user/response/LoginResponse.java @@ -1,4 +1,4 @@ -package org.sopt.app.presentation.user.response; +package org.sopt.app.v1.presentation.user.response; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/org/sopt/app/presentation/user/response/SignUpResponse.java b/src/main/java/org/sopt/app/v1/presentation/user/response/SignUpResponse.java similarity index 74% rename from src/main/java/org/sopt/app/presentation/user/response/SignUpResponse.java rename to src/main/java/org/sopt/app/v1/presentation/user/response/SignUpResponse.java index e62b2a66..e4245b98 100644 --- a/src/main/java/org/sopt/app/presentation/user/response/SignUpResponse.java +++ b/src/main/java/org/sopt/app/v1/presentation/user/response/SignUpResponse.java @@ -1,4 +1,4 @@ -package org.sopt.app.presentation.user.response; +package org.sopt.app.v1.presentation.user.response; import lombok.Builder; import lombok.Getter; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6bf95a0a..eb1f9d9b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,4 +2,8 @@ spring: application: name: app-server profiles: - active: prod \ No newline at end of file + active: prod + +springdoc: + swagger-ui: + path: /swagger-ui.html \ No newline at end of file