diff --git a/README.md b/README.md new file mode 100644 index 000000000..ba4790d12 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# 지하철 3단계 - 회원 관리, 즐겨찾기 + +## 요구 사항 + +- 회원 정보를 관리하는 기능 구현 +- 자신의 정보만 수정 가능하도록 해야하며 **로그인이 선행**되어야 함 +- 토큰의 유효성 검사와 본인 여부를 판단하는 로직 추가 +- side case에 대한 예외처리 +- 인수 테스트와 단위 테스트 작성 +- API 문서를 작성하고 문서화를 위한 테스트 작성 +- 페이지 연동 +- 즐겨찾기 기능을 추가(추가,삭제,조회) +- 자신의 정보만 수정 가능하도록 해야하며 **로그인이 선행**되어야 함 +- 토큰의 유효성 검사와 본인 여부를 판단하는 로직 추가(interceptor, argument resolver) +- side case에 대한 예외처리 필수 +- 인수 테스트와 단위 테스트 작성 +- API 문서를 작성하고 문서화를 위한 테스트 작성 +- 페이지 연동 + +### 기능 목록 + +회원 정보 관리 + +- [x] 회원 가입 +- [x] 로그인 +- [x] 로그인 후 회원 정보 조회/수정/삭제 +- [x] 가입 요청에 대한 validation +- [x] 로그인한 회원 정보를 토큰으로 보유 +- [x] 회원 정보 토큰을 만료 시간이 존재하는 쿠키내에 저장 +- [x] 회원 정보를 요구하는 요청에 대해 토큰을 통해 인증 +- [x] 회원과 관련된 요청에 대한 테스트와 자동 문서화 기능 + +즐겨찾기 관리 + +- [x] 즐겨찾기 추가 +- [x] 즐겨찾기 조회/ 제거 +- [x] 즐겨찾기 추가/삭제 시 로그인 검증 +- [x] 즐겨찾기 추가 시 중복 검증 +- [x] 즐겨찾기 추가 시 역 이름 검증 + +### 예외 사항 + +- Member 예외사항 + - 회원가입 + - 비밀번호 확인 불일치 + - Blank + - 이메일 중복 + - 로그인 + - 비밀번호 불일치 + - 존재하지 않는 이메일 + - Blank + - 정보조회 + - 토큰 없음 + - 정보수정 + - 토큰 없음 + - 비밀번호 불일치 + - 비밀번호 확인 불일치 + - Blank + - 이메일 중복 + - 정보삭제 + - 토큰 없음 + +- Favorite 예외사항 + - 즐겨찾기 추가 + - 토큰 없음 + - 즐겨찾기 중복 + - 부적절한 역 이름 + - 즐겨찾기 목록 조회 + - 토큰 없음 + - 즐겨찾기 제거 + - 토큰 없음 diff --git a/build.gradle b/build.gradle index 35b189610..cf378221a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,8 @@ plugins { - id 'org.springframework.boot' version '2.2.5.RELEASE' - id 'io.spring.dependency-management' version '1.0.9.RELEASE' - id 'java' + id 'org.springframework.boot' version '2.2.5.RELEASE' + id 'io.spring.dependency-management' version '1.0.9.RELEASE' + id "org.asciidoctor.convert" version "1.5.9.2" + id 'java' } group = 'com.example' @@ -9,23 +10,42 @@ version = '0.0.1-SNAPSHOT' sourceCompatibility = '1.8' repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' - implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' - implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0' - implementation 'org.jgrapht:jgrapht-core:1.0.1' - implementation 'io.jsonwebtoken:jjwt:0.9.1' - testImplementation 'io.rest-assured:rest-assured:3.3.0' - testImplementation('org.springframework.boot:spring-boot-starter-test') { - exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' - } - runtimeOnly 'com.h2database:h2' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' + implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' + implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0' + implementation 'org.jgrapht:jgrapht-core:1.0.1' + implementation 'io.jsonwebtoken:jjwt:0.9.1' + testImplementation 'io.rest-assured:rest-assured:3.3.0' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:2.0.4.RELEASE' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + runtimeOnly 'com.h2database:h2' + asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.4.RELEASE' +} + +ext { + snippetsDir = file('build/generated-snippets') } test { - useJUnitPlatform() + useJUnitPlatform() + outputs.dir snippetsDir +} + +asciidoctor { + inputs.dir snippetsDir + dependsOn test +} + +bootJar { + dependsOn asciidoctor + from("${asciidoctor.outputDir}/html5") { + into 'static/docs' + } } diff --git a/src/docs/asciidoc/api-guide.adoc b/src/docs/asciidoc/api-guide.adoc index 89b6e97f8..1e0401e26 100644 --- a/src/docs/asciidoc/api-guide.adoc +++ b/src/docs/asciidoc/api-guide.adoc @@ -5,7 +5,7 @@ endif::[] :icons: font :source-highlighter: highlightjs :toc: left -:toclevels: 2 +:toclevels: 4 :sectlinks: :operation-http-request-title: Example Request :operation-http-response-title: Example Response @@ -19,4 +19,121 @@ endif::[] [[resources-members-create]] === 회원 가입 -operation::members/create[snippets='http-request,http-response'] \ No newline at end of file +[[resources-members-create-success]] +===== 성공 +operation::members/create[snippets='http-request,http-response,request-fields'] + + +[[resources-members-create-fail]] +==== 실패 + +[[resources-members-create-fail-duplicated-email]] +===== 이메일 중복 +operation::members/duplicate-create[snippets='http-request,http-response,request-fields'] + + +[[resources-members-create-fail-not-match-password]] +===== 패스워드 불일치 +operation::members/not-match-password-create[snippets='http-request,http-response,request-fields'] + +[[resources-members-login]] +=== 로그인 + +[[resources-members-login-successful]] +===== 성공 +operation::members/login[snippets='http-request,http-response,request-fields'] + +[[resources-members-login-fail]] +==== 실패 + +[[resources-members-login-fail-not-exist-email]] +===== 존재하지 않는 이메일 +operation::members/login-with-not-exist-email[snippets='http-request,http-response,request-fields'] + +[[resources-members-login-fail-not-match-password]] +===== 비밀번호 불일치 +operation::members/login-with-wrong-password[snippets='http-request,http-response,request-fields'] + +[[resources-members-get]] +=== 회원 정보 조회 + +[[resources-members-get-successful]] +===== 성공 +operation::members/get[snippets='http-request,http-response'] + +[[resources-members-get-fail]] +==== 실패 + +[[resources-members-get-fail-not-login]] +===== 토큰이 존재하지 않음 +operation::members/not-exist-get[snippets='http-request,http-response'] + + +[[resources-members-update]] +=== 회원 정보 수정 + +[[resources-members-update-successful]] +===== 성공 +operation::members/update[snippets='http-request,http-response,request-fields'] + +[[resources-members-update-fail]] +==== 실패 + +[[resources-members-update-fail-not-login]] +===== 토큰이 존재하지 않음 +operation::members/not-exist-token-update[snippets='http-request,http-response,request-fields,response-fields'] + +[[resources-members-update-fail-not-match-password]] +===== 비밀번호가 올바르지 않 +operation::members/not-match-password-update[snippets='http-request,http-response,request-fields,response-fields'] + +[[resources-members-delete]] +=== 회원 탈퇴 + +[[resources-members-delete-successful]] +===== 성공 +operation::members/delete[snippets='http-request,http-response'] + +[[resources-members-delete-fail]] +==== 실패 + +[[resources-members-delete-not-login]] +===== 토큰이 존재하지 않음 +operation::members/delete-not-exist-token[snippets='http-request,http-response,response-fields'] + +[[resources-favorites]] +== 즐겨찾기 + +[[resources-favorites-create]] +=== 즐겨찾기 추가 + +[[resources-favorites-create-successful]] +===== 성공 +operation::favorites/create[snippets='http-request,http-response,request-fields'] + +[[resources-favorites-create-fail]] +==== 실패 + +[[resources-favorites-create-fail-no-login]] +===== 토큰이 존재하지 않음 +operation::favorites/create-no-login[snippets='http-request,http-response,request-fields,response-fields'] + +===== 즐겨찾기 중복 +operation::favorites/create-duplicated[snippets='http-request,http-response,request-fields,response-fields'] + +[[resources-favorites-delete]] +=== 즐겨찾기 삭제 + +[[resources-favorites-delete-successful]] +===== 성공 +operation::favorites/delete[snippets='http-request,http-response'] + +[[resources-favorites-delete-fail]] +==== 실패 + +[[resources-favorites-delete-fail-not-login]] +===== 토큰이 존재하지 않음 +operation::favorites/delete-not-login[snippets='http-request,http-response, response-fields'] + + + diff --git a/src/main/java/wooteco/subway/SubwayAdminApplication.java b/src/main/java/wooteco/subway/SubwayAdminApplication.java index b3e526fab..34839cf01 100644 --- a/src/main/java/wooteco/subway/SubwayAdminApplication.java +++ b/src/main/java/wooteco/subway/SubwayAdminApplication.java @@ -2,7 +2,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.data.jdbc.repository.config.EnableJdbcAuditing; @SpringBootApplication public class SubwayAdminApplication { @@ -10,5 +9,4 @@ public class SubwayAdminApplication { public static void main(String[] args) { SpringApplication.run(SubwayAdminApplication.class, args); } - } diff --git a/src/main/java/wooteco/subway/config/ETagHeaderFilter.java b/src/main/java/wooteco/subway/config/ETagHeaderFilter.java index b1738e542..b6ddc721b 100644 --- a/src/main/java/wooteco/subway/config/ETagHeaderFilter.java +++ b/src/main/java/wooteco/subway/config/ETagHeaderFilter.java @@ -9,7 +9,7 @@ public class ETagHeaderFilter { @Bean public FilterRegistrationBean shallowEtagHeaderFilter() { - FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); filterRegistrationBean.addUrlPatterns("/lines/detail"); filterRegistrationBean.setName("etagFilter"); return filterRegistrationBean; diff --git a/src/main/java/wooteco/subway/config/WebMvcConfig.java b/src/main/java/wooteco/subway/config/WebMvcConfig.java index 87217dc79..36dc21498 100644 --- a/src/main/java/wooteco/subway/config/WebMvcConfig.java +++ b/src/main/java/wooteco/subway/config/WebMvcConfig.java @@ -1,37 +1,30 @@ package wooteco.subway.config; +import java.util.List; + import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import wooteco.subway.web.member.LoginMemberMethodArgumentResolver; -import wooteco.subway.web.member.interceptor.BasicAuthInterceptor; -import wooteco.subway.web.member.interceptor.BearerAuthInterceptor; -import wooteco.subway.web.member.interceptor.SessionInterceptor; -import java.util.List; +import wooteco.subway.web.prehandler.BearerAuthInterceptor; +import wooteco.subway.web.prehandler.LoginMemberMethodArgumentResolver; @Configuration public class WebMvcConfig implements WebMvcConfigurer { - private final BasicAuthInterceptor basicAuthInterceptor; - private final SessionInterceptor sessionInterceptor; private final BearerAuthInterceptor bearerAuthInterceptor; private final LoginMemberMethodArgumentResolver loginMemberArgumentResolver; - public WebMvcConfig(BasicAuthInterceptor basicAuthInterceptor, - SessionInterceptor sessionInterceptor, - BearerAuthInterceptor bearerAuthInterceptor, - LoginMemberMethodArgumentResolver loginMemberArgumentResolver) { - this.basicAuthInterceptor = basicAuthInterceptor; - this.sessionInterceptor = sessionInterceptor; + public WebMvcConfig( + BearerAuthInterceptor bearerAuthInterceptor, + LoginMemberMethodArgumentResolver loginMemberArgumentResolver) { this.bearerAuthInterceptor = bearerAuthInterceptor; this.loginMemberArgumentResolver = loginMemberArgumentResolver; } @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(basicAuthInterceptor).addPathPatterns("/me/basic"); - registry.addInterceptor(sessionInterceptor).addPathPatterns("/me/session"); - registry.addInterceptor(bearerAuthInterceptor).addPathPatterns("/me/bearer"); + registry.addInterceptor(bearerAuthInterceptor). + addPathPatterns("/**"); } @Override diff --git a/src/main/java/wooteco/subway/domain/favorite/Favorite.java b/src/main/java/wooteco/subway/domain/favorite/Favorite.java new file mode 100644 index 000000000..1d6223baf --- /dev/null +++ b/src/main/java/wooteco/subway/domain/favorite/Favorite.java @@ -0,0 +1,51 @@ +package wooteco.subway.domain.favorite; + +import java.util.Objects; + +import org.springframework.data.annotation.Id; + +public class Favorite { + @Id + private Long id; + private Long memberId; + private Long sourceId; + private Long targetId; + + protected Favorite() { + } + + public Favorite(Long id, Long memberId, Long sourceId, Long targetId) { + this.id = id; + this.memberId = memberId; + this.sourceId = sourceId; + this.targetId = targetId; + } + + public static Favorite of(Long id, Long memberId, Long sourceId, Long targetId) { + return new Favorite(id, memberId, sourceId, targetId); + } + + public static Favorite of(Long memberId, Long sourceId, Long targetId) { + return new Favorite(null, memberId, sourceId, targetId); + } + + public Long getId() { + return id; + } + + public Long getMemberId() { + return memberId; + } + + public Long getSourceId() { + return sourceId; + } + + public Long getTargetId() { + return targetId; + } + + public boolean isSameId(Long id) { + return Objects.equals(this.id, id); + } +} diff --git a/src/main/java/wooteco/subway/domain/favorite/FavoriteRepository.java b/src/main/java/wooteco/subway/domain/favorite/FavoriteRepository.java new file mode 100644 index 000000000..791aee302 --- /dev/null +++ b/src/main/java/wooteco/subway/domain/favorite/FavoriteRepository.java @@ -0,0 +1,16 @@ +package wooteco.subway.domain.favorite; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jdbc.repository.query.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; + +public interface FavoriteRepository extends CrudRepository { + @Query("select * from favorite where (id = :id) and (member_id = :memberId)") + Optional findByIdAndMemberId(@Param("id") Long id, @Param("memberId") Long memberId); + + @Query("select * from favorite where member_id = :memberId") + List findAllByMemberId(@Param("memberId") Long memberId); +} diff --git a/src/main/java/wooteco/subway/domain/member/Member.java b/src/main/java/wooteco/subway/domain/member/Member.java index a9adbb264..840abd14a 100644 --- a/src/main/java/wooteco/subway/domain/member/Member.java +++ b/src/main/java/wooteco/subway/domain/member/Member.java @@ -54,4 +54,8 @@ public void update(String name, String password) { public boolean checkPassword(String password) { return this.password.equals(password); } + + public boolean isSameId(Long id) { + return this.id.equals(id); + } } diff --git a/src/main/java/wooteco/subway/domain/station/StationRepository.java b/src/main/java/wooteco/subway/domain/station/StationRepository.java index 93c2d56cb..2a3d33fb8 100644 --- a/src/main/java/wooteco/subway/domain/station/StationRepository.java +++ b/src/main/java/wooteco/subway/domain/station/StationRepository.java @@ -1,12 +1,12 @@ package wooteco.subway.domain.station; +import java.util.List; +import java.util.Optional; + import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; -import java.util.List; -import java.util.Optional; - public interface StationRepository extends CrudRepository { @Override List findAllById(Iterable ids); diff --git a/src/main/java/wooteco/subway/infra/JwtTokenProvider.java b/src/main/java/wooteco/subway/infra/JwtTokenProvider.java index 59e039e71..b1fa55a04 100644 --- a/src/main/java/wooteco/subway/infra/JwtTokenProvider.java +++ b/src/main/java/wooteco/subway/infra/JwtTokenProvider.java @@ -1,11 +1,17 @@ package wooteco.subway.infra; -import io.jsonwebtoken.*; +import java.util.Base64; +import java.util.Date; + import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import java.util.Base64; -import java.util.Date; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import wooteco.subway.web.exception.InvalidAuthenticationException; @Component public class JwtTokenProvider { @@ -41,13 +47,12 @@ public boolean validateToken(String token) { Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); if (claims.getBody().getExpiration().before(new Date())) { - return false; + throw new InvalidAuthenticationException("토큰이 만료되었습니다. 다시 로그인 해주세요."); } - - return true; } catch (JwtException | IllegalArgumentException e) { - return false; + throw new InvalidAuthenticationException("로그인 후 이용해주세요."); } + return true; } } diff --git a/src/main/java/wooteco/subway/service/member/MemberService.java b/src/main/java/wooteco/subway/service/member/MemberService.java deleted file mode 100644 index 12f3c2a12..000000000 --- a/src/main/java/wooteco/subway/service/member/MemberService.java +++ /dev/null @@ -1,51 +0,0 @@ -package wooteco.subway.service.member; - -import org.springframework.stereotype.Service; -import wooteco.subway.domain.member.Member; -import wooteco.subway.domain.member.MemberRepository; -import wooteco.subway.infra.JwtTokenProvider; -import wooteco.subway.service.member.dto.LoginRequest; -import wooteco.subway.service.member.dto.UpdateMemberRequest; - -@Service -public class MemberService { - private MemberRepository memberRepository; - private JwtTokenProvider jwtTokenProvider; - - public MemberService(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider) { - this.memberRepository = memberRepository; - this.jwtTokenProvider = jwtTokenProvider; - } - - public Member createMember(Member member) { - return memberRepository.save(member); - } - - public void updateMember(Long id, UpdateMemberRequest param) { - Member member = memberRepository.findById(id).orElseThrow(RuntimeException::new); - member.update(param.getName(), param.getPassword()); - memberRepository.save(member); - } - - public void deleteMember(Long id) { - memberRepository.deleteById(id); - } - - public String createToken(LoginRequest param) { - Member member = memberRepository.findByEmail(param.getEmail()).orElseThrow(RuntimeException::new); - if (!member.checkPassword(param.getPassword())) { - throw new RuntimeException("잘못된 패스워드"); - } - - return jwtTokenProvider.createToken(param.getEmail()); - } - - public Member findMemberByEmail(String email) { - return memberRepository.findByEmail(email).orElseThrow(RuntimeException::new); - } - - public boolean loginWithForm(String email, String password) { - Member member = findMemberByEmail(email); - return member.checkPassword(password); - } -} diff --git a/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java b/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java deleted file mode 100644 index 05aed3875..000000000 --- a/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java +++ /dev/null @@ -1,25 +0,0 @@ -package wooteco.subway.service.member.dto; - -import wooteco.subway.domain.member.Member; - -public class MemberRequest { - private String email; - private String name; - private String password; - - public String getEmail() { - return email; - } - - public String getName() { - return name; - } - - public String getPassword() { - return password; - } - - public Member toMember() { - return new Member(email, name, password); - } -} diff --git a/src/main/java/wooteco/subway/service/member/dto/UpdateMemberRequest.java b/src/main/java/wooteco/subway/service/member/dto/UpdateMemberRequest.java deleted file mode 100644 index d847ccf2b..000000000 --- a/src/main/java/wooteco/subway/service/member/dto/UpdateMemberRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -package wooteco.subway.service.member.dto; - -public class UpdateMemberRequest { - private String name; - private String password; - - public UpdateMemberRequest() { - } - - public UpdateMemberRequest(String name, String password) { - this.name = name; - this.password = password; - } - - public String getName() { - return name; - } - - public String getPassword() { - return password; - } -} diff --git a/src/main/java/wooteco/subway/service/station/StationService.java b/src/main/java/wooteco/subway/service/station/StationService.java index a2d6d83da..871d67eac 100644 --- a/src/main/java/wooteco/subway/service/station/StationService.java +++ b/src/main/java/wooteco/subway/service/station/StationService.java @@ -1,18 +1,22 @@ package wooteco.subway.service.station; +import java.util.Collection; +import java.util.List; + import org.springframework.stereotype.Service; -import wooteco.subway.service.line.LineStationService; + import wooteco.subway.domain.station.Station; import wooteco.subway.domain.station.StationRepository; - -import java.util.List; +import wooteco.subway.service.line.LineStationService; +import wooteco.subway.web.exception.NotFoundStationException; @Service public class StationService { private LineStationService lineStationService; private StationRepository stationRepository; - public StationService(LineStationService lineStationService, StationRepository stationRepository) { + public StationService(LineStationService lineStationService, + StationRepository stationRepository) { this.lineStationService = lineStationService; this.stationRepository = stationRepository; } @@ -29,4 +33,14 @@ public void deleteStationById(Long id) { lineStationService.deleteLineStationByStationId(id); stationRepository.deleteById(id); } + + public Long findStationIdByName(String name) { + return stationRepository.findByName(name) + .map(Station::getId) + .orElseThrow(() -> new NotFoundStationException(name + "역을 찾을 수 없습니다.")); + } + + public List findAllById(Collection ids) { + return stationRepository.findAllById(ids); + } } diff --git a/src/main/java/wooteco/subway/web/controller/FavoriteController.java b/src/main/java/wooteco/subway/web/controller/FavoriteController.java new file mode 100644 index 000000000..2f42f47a8 --- /dev/null +++ b/src/main/java/wooteco/subway/web/controller/FavoriteController.java @@ -0,0 +1,43 @@ +package wooteco.subway.web.controller; + +import java.net.URI; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import wooteco.subway.domain.member.Member; +import wooteco.subway.web.prehandler.IsAuth; +import wooteco.subway.web.prehandler.LoginMember; +import wooteco.subway.web.service.favorite.FavoriteService; +import wooteco.subway.web.service.favorite.dto.FavoriteRequest; +import wooteco.subway.web.service.favorite.dto.FavoriteResponse; + +@RestController +public class FavoriteController { + private final FavoriteService favoriteService; + + public FavoriteController(FavoriteService favoriteService) { + this.favoriteService = favoriteService; + } + + @IsAuth + @PostMapping("/favorites") + public ResponseEntity create(@LoginMember Member member, + @RequestBody FavoriteRequest favoriteRequest) { + FavoriteResponse persistFavorite = favoriteService.create(member, favoriteRequest); + return ResponseEntity.created(URI.create( + "/members/" + persistFavorite.getMemberId() + "/favorites/" + persistFavorite.getId())) + .build(); + } + + @IsAuth + @DeleteMapping("/favorites/{id}") + public ResponseEntity delete(@LoginMember Member member, @PathVariable Long id) { + favoriteService.delete(member, id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/wooteco/subway/web/controller/GlobalMemberExceptionHandler.java b/src/main/java/wooteco/subway/web/controller/GlobalMemberExceptionHandler.java new file mode 100644 index 000000000..218ed1011 --- /dev/null +++ b/src/main/java/wooteco/subway/web/controller/GlobalMemberExceptionHandler.java @@ -0,0 +1,68 @@ +package wooteco.subway.web.controller; + +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.dao.DataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import wooteco.subway.web.exception.AuthenticationException; +import wooteco.subway.web.exception.BusinessException; +import wooteco.subway.web.exception.ErrorResponse; + +@RestControllerAdvice +public class GlobalMemberExceptionHandler { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + @ExceptionHandler(value = MethodArgumentNotValidException.class) + public ResponseEntity handleRequestException(MethodArgumentNotValidException e) { + BindingResult bindingResult = e.getBindingResult(); + List errorMessages = bindingResult.getAllErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.toList()); + + return ResponseEntity.badRequest().body(new ErrorResponse(errorMessages.get(0))); + } + + @ExceptionHandler(value = AuthenticationException.class) + public ResponseEntity handleAuthorizeException(AuthenticationException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(new ErrorResponse(e.getMessage())); + } + + @ExceptionHandler(value = BusinessException.class) + public ResponseEntity handlerBusinessException(BusinessException e) { + return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage())); + } + + @ExceptionHandler(value = DataAccessException.class) + public ResponseEntity handlerDataAccessException(DataAccessException e) { + return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage())); + } + + @ExceptionHandler(value = RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException e) { + logger.info("예상치 못한 Runtime Exception", e); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse()); + } + + @ExceptionHandler(value = Exception.class) + public ResponseEntity handleUnexpectedException(Exception e) { + logger.info("예상치 못한 Exception", e); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponse()); + } +} diff --git a/src/main/java/wooteco/subway/web/LineController.java b/src/main/java/wooteco/subway/web/controller/LineController.java similarity index 84% rename from src/main/java/wooteco/subway/web/LineController.java rename to src/main/java/wooteco/subway/web/controller/LineController.java index 829b2a013..c9b94ef69 100644 --- a/src/main/java/wooteco/subway/web/LineController.java +++ b/src/main/java/wooteco/subway/web/controller/LineController.java @@ -1,17 +1,24 @@ -package wooteco.subway.web; +package wooteco.subway.web.controller; + +import java.net.URI; +import java.util.List; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +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.RestController; + +import wooteco.subway.domain.line.Line; import wooteco.subway.service.line.LineService; import wooteco.subway.service.line.dto.LineDetailResponse; import wooteco.subway.service.line.dto.LineRequest; import wooteco.subway.service.line.dto.LineResponse; import wooteco.subway.service.line.dto.LineStationCreateRequest; import wooteco.subway.service.line.dto.WholeSubwayResponse; -import wooteco.subway.domain.line.Line; - -import java.net.URI; -import java.util.List; @RestController public class LineController { diff --git a/src/main/java/wooteco/subway/web/controller/LoginMemberController.java b/src/main/java/wooteco/subway/web/controller/LoginMemberController.java new file mode 100644 index 000000000..c6f64b0c3 --- /dev/null +++ b/src/main/java/wooteco/subway/web/controller/LoginMemberController.java @@ -0,0 +1,58 @@ +package wooteco.subway.web.controller; + +import java.util.Set; + +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.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import wooteco.subway.domain.member.Member; +import wooteco.subway.web.prehandler.IsAuth; +import wooteco.subway.web.prehandler.LoginMember; +import wooteco.subway.web.service.favorite.FavoriteService; +import wooteco.subway.web.service.favorite.dto.FavoriteDetailResponse; +import wooteco.subway.web.service.member.MemberService; +import wooteco.subway.web.service.member.dto.LoginRequest; +import wooteco.subway.web.service.member.dto.MemberDetailResponse; +import wooteco.subway.web.service.member.dto.MemberResponse; +import wooteco.subway.web.service.member.dto.TokenResponse; + +@RestController +public class LoginMemberController { + private final MemberService memberService; + private final FavoriteService favoriteService; + + public LoginMemberController(MemberService memberService, + FavoriteService favoriteService) { + this.memberService = memberService; + this.favoriteService = favoriteService; + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest param) { + String token = memberService.createToken(param); + return ResponseEntity.ok().body(new TokenResponse(token, "bearer")); + } + + @IsAuth + @GetMapping("/me") + public ResponseEntity getMemberOfMineBasic(@LoginMember Member member) { + return ResponseEntity.ok().body(MemberResponse.of(member)); + } + + @IsAuth + @GetMapping("/me/detail") + public ResponseEntity getMemberDetailOfMineBasic(@LoginMember Member member) { + Set responses = favoriteService.getAll(member); + return ResponseEntity.ok().body(MemberDetailResponse.of(member, responses)); + } + + @IsAuth + @GetMapping("/me/favorites") + public ResponseEntity> getMemberFavorites(@LoginMember Member member) { + Set responses = favoriteService.getAll(member); + return ResponseEntity.ok().body(responses); + } +} diff --git a/src/main/java/wooteco/subway/web/controller/MemberController.java b/src/main/java/wooteco/subway/web/controller/MemberController.java new file mode 100644 index 000000000..e81df8706 --- /dev/null +++ b/src/main/java/wooteco/subway/web/controller/MemberController.java @@ -0,0 +1,62 @@ +package wooteco.subway.web.controller; + +import java.net.URI; + +import javax.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import wooteco.subway.domain.member.Member; +import wooteco.subway.web.prehandler.IsAuth; +import wooteco.subway.web.service.member.MemberService; +import wooteco.subway.web.service.member.dto.MemberRequest; +import wooteco.subway.web.service.member.dto.MemberResponse; +import wooteco.subway.web.service.member.dto.UpdateMemberRequest; + +@RestController +public class MemberController { + private final MemberService memberService; + + public MemberController(MemberService memberService) { + this.memberService = memberService; + } + + @PostMapping("/members") + public ResponseEntity createMember(@Validated @RequestBody MemberRequest request) { + Member member = memberService.save(request.toMember()); + return ResponseEntity + .created(URI.create("/members/" + member.getId())) + .build(); + } + + @IsAuth + @GetMapping("/members") + public ResponseEntity getMemberByEmail(@RequestParam String email) { + Member member = memberService.findMemberByEmail(email); + return ResponseEntity.ok().body(MemberResponse.of(member)); + } + + @IsAuth + @PutMapping("/members/{id}") + public ResponseEntity updateMember(@PathVariable Long id, + @Valid @RequestBody UpdateMemberRequest param) { + memberService.updateMember(id, param); + return ResponseEntity.ok().build(); + } + + @IsAuth + @DeleteMapping("/members/{id}") + public ResponseEntity deleteMember(@PathVariable Long id) { + memberService.deleteMember(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/wooteco/subway/web/PageController.java b/src/main/java/wooteco/subway/web/controller/PageController.java similarity index 81% rename from src/main/java/wooteco/subway/web/PageController.java rename to src/main/java/wooteco/subway/web/controller/PageController.java index c60345782..cb5cc4351 100644 --- a/src/main/java/wooteco/subway/web/PageController.java +++ b/src/main/java/wooteco/subway/web/controller/PageController.java @@ -1,9 +1,10 @@ -package wooteco.subway.web; +package wooteco.subway.web.controller; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; + import wooteco.subway.service.line.LineService; import wooteco.subway.service.station.StationService; @@ -19,6 +20,11 @@ public PageController(LineService lineService, StationService stationService) { @GetMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE) public String index() { + return "service/index"; + } + + @GetMapping(value = "/admin", produces = MediaType.TEXT_HTML_VALUE) + public String admin() { return "admin/index"; } @@ -63,4 +69,14 @@ public String loginPage() { public String favoritesPage() { return "service/favorite"; } + + @GetMapping(value = "/my-page", produces = MediaType.TEXT_HTML_VALUE) + public String myPage() { + return "service/mypage"; + } + + @GetMapping(value = "/mypage-edit", produces = MediaType.TEXT_HTML_VALUE) + public String myPageEdit() { + return "service/mypage-edit"; + } } diff --git a/src/main/java/wooteco/subway/web/PathController.java b/src/main/java/wooteco/subway/web/controller/PathController.java similarity index 95% rename from src/main/java/wooteco/subway/web/PathController.java rename to src/main/java/wooteco/subway/web/controller/PathController.java index b8cb78de1..6f90262ba 100644 --- a/src/main/java/wooteco/subway/web/PathController.java +++ b/src/main/java/wooteco/subway/web/controller/PathController.java @@ -1,9 +1,10 @@ -package wooteco.subway.web; +package wooteco.subway.web.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; + import wooteco.subway.domain.path.PathType; import wooteco.subway.service.path.PathService; diff --git a/src/main/java/wooteco/subway/web/StationController.java b/src/main/java/wooteco/subway/web/controller/StationController.java similarity index 76% rename from src/main/java/wooteco/subway/web/StationController.java rename to src/main/java/wooteco/subway/web/controller/StationController.java index fdf7b69d2..8111f2590 100644 --- a/src/main/java/wooteco/subway/web/StationController.java +++ b/src/main/java/wooteco/subway/web/controller/StationController.java @@ -1,14 +1,20 @@ -package wooteco.subway.web; +package wooteco.subway.web.controller; + +import java.net.URI; +import java.util.List; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +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.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import wooteco.subway.domain.station.Station; import wooteco.subway.service.station.StationService; import wooteco.subway.service.station.dto.StationCreateRequest; import wooteco.subway.service.station.dto.StationResponse; -import wooteco.subway.domain.station.Station; - -import java.net.URI; -import java.util.List; @RestController public class StationController { diff --git a/src/main/java/wooteco/subway/web/exception/AuthenticationException.java b/src/main/java/wooteco/subway/web/exception/AuthenticationException.java new file mode 100644 index 000000000..b3721e3ec --- /dev/null +++ b/src/main/java/wooteco/subway/web/exception/AuthenticationException.java @@ -0,0 +1,15 @@ +package wooteco.subway.web.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.UNAUTHORIZED) +public class AuthenticationException extends RuntimeException{ + public AuthenticationException() { + super(); + } + + public AuthenticationException(String message) { + super(message); + } +} diff --git a/src/main/java/wooteco/subway/web/exception/BusinessException.java b/src/main/java/wooteco/subway/web/exception/BusinessException.java new file mode 100644 index 000000000..eeee8d3df --- /dev/null +++ b/src/main/java/wooteco/subway/web/exception/BusinessException.java @@ -0,0 +1,11 @@ +package wooteco.subway.web.exception; + +public class BusinessException extends RuntimeException{ + public BusinessException() { + super(); + } + + public BusinessException(String message) { + super(message); + } +} diff --git a/src/main/java/wooteco/subway/web/exception/DuplicatedEmailException.java b/src/main/java/wooteco/subway/web/exception/DuplicatedEmailException.java new file mode 100644 index 000000000..86e7bcaad --- /dev/null +++ b/src/main/java/wooteco/subway/web/exception/DuplicatedEmailException.java @@ -0,0 +1,7 @@ +package wooteco.subway.web.exception; + +public class DuplicatedEmailException extends BusinessException { + public DuplicatedEmailException(String email) { + super(email + " 이메일이 중복되었습니다."); + } +} diff --git a/src/main/java/wooteco/subway/web/exception/DuplicatedFavoriteException.java b/src/main/java/wooteco/subway/web/exception/DuplicatedFavoriteException.java new file mode 100644 index 000000000..faba396dc --- /dev/null +++ b/src/main/java/wooteco/subway/web/exception/DuplicatedFavoriteException.java @@ -0,0 +1,7 @@ +package wooteco.subway.web.exception; + +public class DuplicatedFavoriteException extends BusinessException { + public DuplicatedFavoriteException(Long sourceId, Long targetId) { + super(sourceId + "번 역에서 " + targetId + "번 역까지 가는 경로는 이미 추가하셨습니다."); + } +} diff --git a/src/main/java/wooteco/subway/web/exception/ErrorResponse.java b/src/main/java/wooteco/subway/web/exception/ErrorResponse.java new file mode 100644 index 000000000..96dfd0242 --- /dev/null +++ b/src/main/java/wooteco/subway/web/exception/ErrorResponse.java @@ -0,0 +1,20 @@ +package wooteco.subway.web.exception; + +public class ErrorResponse { + + private static final String DEFAULT_MESSAGE = "알 수 없는 에러가 발생했습니다."; + + private String message; + + public ErrorResponse() { + this.message = DEFAULT_MESSAGE; + } + + public ErrorResponse(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/wooteco/subway/web/exception/InvalidAuthenticationException.java b/src/main/java/wooteco/subway/web/exception/InvalidAuthenticationException.java new file mode 100644 index 000000000..01b8ca1f8 --- /dev/null +++ b/src/main/java/wooteco/subway/web/exception/InvalidAuthenticationException.java @@ -0,0 +1,7 @@ +package wooteco.subway.web.exception; + +public class InvalidAuthenticationException extends AuthenticationException{ + public InvalidAuthenticationException(String message) { + super(message); + } +} diff --git a/src/main/java/wooteco/subway/web/exception/InvalidTokenException.java b/src/main/java/wooteco/subway/web/exception/InvalidTokenException.java new file mode 100644 index 000000000..aa9605933 --- /dev/null +++ b/src/main/java/wooteco/subway/web/exception/InvalidTokenException.java @@ -0,0 +1,7 @@ +package wooteco.subway.web.exception; + +public class InvalidTokenException extends AuthenticationException { + public InvalidTokenException(String message) { + super(message); + } +} diff --git a/src/main/java/wooteco/subway/web/exception/NotFoundFavoriteException.java b/src/main/java/wooteco/subway/web/exception/NotFoundFavoriteException.java new file mode 100644 index 000000000..b78b1b4d5 --- /dev/null +++ b/src/main/java/wooteco/subway/web/exception/NotFoundFavoriteException.java @@ -0,0 +1,13 @@ +package wooteco.subway.web.exception; + +public class NotFoundFavoriteException extends AuthenticationException { + public static final String ERROR_MESSAGE = " 즐겨찾기를 찾을 수 없습니다."; + + public NotFoundFavoriteException() { + super(ERROR_MESSAGE); + } + + public NotFoundFavoriteException(Long id) { + super(id + ERROR_MESSAGE); + } +} diff --git a/src/main/java/wooteco/subway/web/exception/NotFoundMemberException.java b/src/main/java/wooteco/subway/web/exception/NotFoundMemberException.java new file mode 100644 index 000000000..89a3191a8 --- /dev/null +++ b/src/main/java/wooteco/subway/web/exception/NotFoundMemberException.java @@ -0,0 +1,14 @@ +package wooteco.subway.web.exception; + +public class NotFoundMemberException extends AuthenticationException { + + public static final String ERROR_MESSAGE = " 회원님을 찾을 수 없습니다."; + + public NotFoundMemberException() { + super(ERROR_MESSAGE); + } + + public NotFoundMemberException(String email) { + super(email + ERROR_MESSAGE); + } +} diff --git a/src/main/java/wooteco/subway/web/exception/NotFoundStationException.java b/src/main/java/wooteco/subway/web/exception/NotFoundStationException.java new file mode 100644 index 000000000..d16e874c1 --- /dev/null +++ b/src/main/java/wooteco/subway/web/exception/NotFoundStationException.java @@ -0,0 +1,9 @@ +package wooteco.subway.web.exception; + +import org.springframework.dao.DataAccessException; + +public class NotFoundStationException extends DataAccessException { + public NotFoundStationException(String message) { + super(message); + } +} diff --git a/src/main/java/wooteco/subway/web/exception/NotMatchMemberException.java b/src/main/java/wooteco/subway/web/exception/NotMatchMemberException.java new file mode 100644 index 000000000..491a5cda8 --- /dev/null +++ b/src/main/java/wooteco/subway/web/exception/NotMatchMemberException.java @@ -0,0 +1,9 @@ +package wooteco.subway.web.exception; + +public class NotMatchMemberException extends AuthenticationException { + private static final String ERROR_MESSAGE = "회원이 일치하지 않습니다."; + + public NotMatchMemberException() { + super(ERROR_MESSAGE); + } +} diff --git a/src/main/java/wooteco/subway/web/exception/NotMatchPasswordException.java b/src/main/java/wooteco/subway/web/exception/NotMatchPasswordException.java new file mode 100644 index 000000000..cea422761 --- /dev/null +++ b/src/main/java/wooteco/subway/web/exception/NotMatchPasswordException.java @@ -0,0 +1,14 @@ +package wooteco.subway.web.exception; + +public class NotMatchPasswordException extends AuthenticationException { + + private static final String ERROR_MESSAGE = "패스워드가 일치하지 않습니다."; + + public NotMatchPasswordException() { + super(ERROR_MESSAGE); + } + + public NotMatchPasswordException(String message) { + super(message); + } +} diff --git a/src/main/java/wooteco/subway/web/member/AuthorizationExtractor.java b/src/main/java/wooteco/subway/web/member/AuthorizationExtractor.java deleted file mode 100644 index 479af4fdf..000000000 --- a/src/main/java/wooteco/subway/web/member/AuthorizationExtractor.java +++ /dev/null @@ -1,31 +0,0 @@ -package wooteco.subway.web.member; - -import org.apache.logging.log4j.util.Strings; -import org.springframework.stereotype.Component; - -import javax.servlet.http.HttpServletRequest; -import java.util.Enumeration; - -@Component -public class AuthorizationExtractor { - public static final String AUTHORIZATION = "Authorization"; - public static final String ACCESS_TOKEN_TYPE = AuthorizationExtractor.class.getSimpleName() + ".ACCESS_TOKEN_TYPE"; - - public String extract(HttpServletRequest request, String type) { - Enumeration headers = request.getHeaders(AUTHORIZATION); - while (headers.hasMoreElements()) { - String value = headers.nextElement(); - if ((value.toLowerCase().startsWith(type.toLowerCase()))) { - String authHeaderValue = value.substring(type.length()).trim(); - request.setAttribute(ACCESS_TOKEN_TYPE, value.substring(0, type.length()).trim()); - int commaIndex = authHeaderValue.indexOf(','); - if (commaIndex > 0) { - authHeaderValue = authHeaderValue.substring(0, commaIndex); - } - return authHeaderValue; - } - } - - return Strings.EMPTY; - } -} diff --git a/src/main/java/wooteco/subway/web/member/InvalidAuthenticationException.java b/src/main/java/wooteco/subway/web/member/InvalidAuthenticationException.java deleted file mode 100644 index d0cabdf17..000000000 --- a/src/main/java/wooteco/subway/web/member/InvalidAuthenticationException.java +++ /dev/null @@ -1,11 +0,0 @@ -package wooteco.subway.web.member; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.UNAUTHORIZED) -public class InvalidAuthenticationException extends RuntimeException{ - public InvalidAuthenticationException(String message) { - super(message); - } -} diff --git a/src/main/java/wooteco/subway/web/member/LoginMemberController.java b/src/main/java/wooteco/subway/web/member/LoginMemberController.java deleted file mode 100644 index 9eb4f13a2..000000000 --- a/src/main/java/wooteco/subway/web/member/LoginMemberController.java +++ /dev/null @@ -1,45 +0,0 @@ -package wooteco.subway.web.member; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import wooteco.subway.domain.member.Member; -import wooteco.subway.service.member.MemberService; -import wooteco.subway.service.member.dto.LoginRequest; -import wooteco.subway.service.member.dto.MemberResponse; -import wooteco.subway.service.member.dto.TokenResponse; - -import javax.servlet.http.HttpSession; -import java.util.Map; - -@RestController -public class LoginMemberController { - private MemberService memberService; - - public LoginMemberController(MemberService memberService) { - this.memberService = memberService; - } - - @PostMapping("/oauth/token") - public ResponseEntity login(@RequestBody LoginRequest param) { - String token = memberService.createToken(param); - return ResponseEntity.ok().body(new TokenResponse(token, "bearer")); - } - - @PostMapping("/login") - public ResponseEntity login(@RequestParam Map paramMap, HttpSession session) { - String email = paramMap.get("email"); - String password = paramMap.get("password"); - if (!memberService.loginWithForm(email, password)) { - throw new InvalidAuthenticationException("올바르지 않은 이메일과 비밀번호 입력"); - } - - session.setAttribute("loginMemberEmail", email); - - return ResponseEntity.ok().build(); - } - - @GetMapping({"/me/basic", "/me/session", "/me/bearer"}) - public ResponseEntity getMemberOfMineBasic(@LoginMember Member member) { - return ResponseEntity.ok().body(MemberResponse.of(member)); - } -} diff --git a/src/main/java/wooteco/subway/web/member/MemberController.java b/src/main/java/wooteco/subway/web/member/MemberController.java deleted file mode 100644 index 8a3046948..000000000 --- a/src/main/java/wooteco/subway/web/member/MemberController.java +++ /dev/null @@ -1,46 +0,0 @@ -package wooteco.subway.web.member; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import wooteco.subway.domain.member.Member; -import wooteco.subway.service.member.MemberService; -import wooteco.subway.service.member.dto.MemberRequest; -import wooteco.subway.service.member.dto.MemberResponse; -import wooteco.subway.service.member.dto.UpdateMemberRequest; - -import java.net.URI; - -@RestController -public class MemberController { - private MemberService memberService; - - public MemberController(MemberService memberService) { - this.memberService = memberService; - } - - @PostMapping("/members") - public ResponseEntity createMember(@RequestBody MemberRequest view) { - Member member = memberService.createMember(view.toMember()); - return ResponseEntity - .created(URI.create("/members/" + member.getId())) - .build(); - } - - @GetMapping("/members") - public ResponseEntity getMemberByEmail(@RequestParam String email) { - Member member = memberService.findMemberByEmail(email); - return ResponseEntity.ok().body(MemberResponse.of(member)); - } - - @PutMapping("/members/{id}") - public ResponseEntity updateMember(@PathVariable Long id, @RequestBody UpdateMemberRequest param) { - memberService.updateMember(id, param); - return ResponseEntity.ok().build(); - } - - @DeleteMapping("/members/{id}") - public ResponseEntity deleteMember(@PathVariable Long id) { - memberService.deleteMember(id); - return ResponseEntity.noContent().build(); - } -} diff --git a/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java b/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java deleted file mode 100644 index a5f3a481b..000000000 --- a/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java +++ /dev/null @@ -1,40 +0,0 @@ -package wooteco.subway.web.member.interceptor; - -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; -import wooteco.subway.domain.member.Member; -import wooteco.subway.service.member.MemberService; -import wooteco.subway.web.member.AuthorizationExtractor; -import wooteco.subway.web.member.InvalidAuthenticationException; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -@Component -public class BasicAuthInterceptor implements HandlerInterceptor { - private AuthorizationExtractor authExtractor; - private MemberService memberService; - - public BasicAuthInterceptor(AuthorizationExtractor authExtractor, MemberService memberService) { - this.authExtractor = authExtractor; - this.memberService = memberService; - } - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - // TODO: Authorization 헤더를 통해 Basic 값을 추출 (authExtractor.extract() 메서드 활용) - - // TODO: 추출한 Basic 값을 Base64를 통해 email과 password 값 추출(Base64.getDecoder().decode() 메서드 활용) - - String email = ""; - String password = ""; - - Member member = memberService.findMemberByEmail(email); - if (!member.checkPassword(password)) { - throw new InvalidAuthenticationException("올바르지 않은 이메일과 비밀번호 입력"); - } - - request.setAttribute("loginMemberEmail", email); - return true; - } -} diff --git a/src/main/java/wooteco/subway/web/member/interceptor/SessionInterceptor.java b/src/main/java/wooteco/subway/web/member/interceptor/SessionInterceptor.java deleted file mode 100644 index 1c90fdbbd..000000000 --- a/src/main/java/wooteco/subway/web/member/interceptor/SessionInterceptor.java +++ /dev/null @@ -1,21 +0,0 @@ -package wooteco.subway.web.member.interceptor; - -import org.apache.logging.log4j.util.Strings; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -@Component -public class SessionInterceptor implements HandlerInterceptor { - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - String email = (String) request.getSession().getAttribute("loginMemberEmail"); - if (Strings.isNotBlank(email)) { - request.setAttribute("loginMemberEmail", email); - } - - return true; - } -} diff --git a/src/main/java/wooteco/subway/web/prehandler/AuthorizationExtractor.java b/src/main/java/wooteco/subway/web/prehandler/AuthorizationExtractor.java new file mode 100644 index 000000000..c30bdd77c --- /dev/null +++ b/src/main/java/wooteco/subway/web/prehandler/AuthorizationExtractor.java @@ -0,0 +1,28 @@ +package wooteco.subway.web.prehandler; + +import java.util.Arrays; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.stereotype.Component; + +import wooteco.subway.web.exception.InvalidTokenException; + +@Component +public class AuthorizationExtractor { + + public static final String TOKEN_NAME = "token"; + + public String extract(HttpServletRequest request) { + try { + return Arrays.stream(request.getCookies()) + .filter(cookie -> cookie.getName().equals(TOKEN_NAME)) + .map(Cookie::getValue) + .findFirst() + .orElseThrow(() -> new InvalidTokenException("토큰을 찾을 수 없습니다.")); + } catch (Exception e) { + throw new InvalidTokenException("토큰을 찾을 수 없습니다."); + } + } +} diff --git a/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java b/src/main/java/wooteco/subway/web/prehandler/BearerAuthInterceptor.java similarity index 59% rename from src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java rename to src/main/java/wooteco/subway/web/prehandler/BearerAuthInterceptor.java index bd3c1bb53..c24d5a235 100644 --- a/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java +++ b/src/main/java/wooteco/subway/web/prehandler/BearerAuthInterceptor.java @@ -1,13 +1,18 @@ -package wooteco.subway.web.member.interceptor; +package wooteco.subway.web.prehandler; + +import java.lang.annotation.Annotation; +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; -import wooteco.subway.infra.JwtTokenProvider; -import wooteco.subway.web.member.AuthorizationExtractor; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import wooteco.subway.infra.JwtTokenProvider; @Component public class BearerAuthInterceptor implements HandlerInterceptor { @@ -22,14 +27,13 @@ public BearerAuthInterceptor(AuthorizationExtractor authExtractor, JwtTokenProvi @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - // TODO: Authorization 헤더를 통해 Bearer 값을 추출 (authExtractor.extract() 메서드 활용) - - // TODO: 추출한 토큰값의 유효성 검사 (jwtTokenProvider.validateToken() 메서드 활용) - - // TODO: 추출한 토큰값에서 email 정보 추출 (jwtTokenProvider.getSubject() 메서드 활용) - String email = ""; - - request.setAttribute("loginMemberEmail", email); + IsAuth annotation = getAnnotation((HandlerMethod)handler, IsAuth.class); + if (!ObjectUtils.isEmpty(annotation)) { + String bearer = authExtractor.extract(request); + jwtTokenProvider.validateToken(bearer); + String email = jwtTokenProvider.getSubject(bearer); + request.setAttribute("loginMemberEmail", email); + } return true; } @@ -45,4 +49,9 @@ public void postHandle(HttpServletRequest request, public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } + + private A getAnnotation(HandlerMethod handlerMethod, Class annotationType) { + return Optional.ofNullable(handlerMethod.getMethodAnnotation(annotationType)) + .orElse(handlerMethod.getBeanType().getAnnotation(annotationType)); + } } diff --git a/src/main/java/wooteco/subway/web/prehandler/IsAuth.java b/src/main/java/wooteco/subway/web/prehandler/IsAuth.java new file mode 100644 index 000000000..8cc1ae4f2 --- /dev/null +++ b/src/main/java/wooteco/subway/web/prehandler/IsAuth.java @@ -0,0 +1,12 @@ +package wooteco.subway.web.prehandler; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface IsAuth { +} + diff --git a/src/main/java/wooteco/subway/web/member/LoginMember.java b/src/main/java/wooteco/subway/web/prehandler/LoginMember.java similarity index 87% rename from src/main/java/wooteco/subway/web/member/LoginMember.java rename to src/main/java/wooteco/subway/web/prehandler/LoginMember.java index 8ddcafd88..fdd5b45d6 100644 --- a/src/main/java/wooteco/subway/web/member/LoginMember.java +++ b/src/main/java/wooteco/subway/web/prehandler/LoginMember.java @@ -1,4 +1,4 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.prehandler; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/src/main/java/wooteco/subway/web/member/LoginMemberMethodArgumentResolver.java b/src/main/java/wooteco/subway/web/prehandler/LoginMemberMethodArgumentResolver.java similarity index 88% rename from src/main/java/wooteco/subway/web/member/LoginMemberMethodArgumentResolver.java rename to src/main/java/wooteco/subway/web/prehandler/LoginMemberMethodArgumentResolver.java index 592faba51..afd84393b 100644 --- a/src/main/java/wooteco/subway/web/member/LoginMemberMethodArgumentResolver.java +++ b/src/main/java/wooteco/subway/web/prehandler/LoginMemberMethodArgumentResolver.java @@ -1,4 +1,6 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.prehandler; + +import static org.springframework.web.context.request.RequestAttributes.*; import org.apache.commons.lang3.StringUtils; import org.springframework.core.MethodParameter; @@ -7,10 +9,10 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; -import wooteco.subway.domain.member.Member; -import wooteco.subway.service.member.MemberService; -import static org.springframework.web.context.request.RequestAttributes.SCOPE_REQUEST; +import wooteco.subway.domain.member.Member; +import wooteco.subway.web.exception.InvalidAuthenticationException; +import wooteco.subway.web.service.member.MemberService; @Component public class LoginMemberMethodArgumentResolver implements HandlerMethodArgumentResolver { @@ -35,7 +37,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m try { return memberService.findMemberByEmail(email); } catch (Exception e) { - throw new InvalidAuthenticationException("비정상적인 로그인"); + throw new InvalidAuthenticationException("비정상적인 로그인 입니다."); } } } diff --git a/src/main/java/wooteco/subway/web/prehandler/validator/DuplicateCheck.java b/src/main/java/wooteco/subway/web/prehandler/validator/DuplicateCheck.java new file mode 100644 index 000000000..365062fce --- /dev/null +++ b/src/main/java/wooteco/subway/web/prehandler/validator/DuplicateCheck.java @@ -0,0 +1,20 @@ +package wooteco.subway.web.prehandler.validator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +@Constraint(validatedBy = EmailMatchValidator.class) +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DuplicateCheck { + String message() default ""; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/wooteco/subway/web/prehandler/validator/EmailMatchValidator.java b/src/main/java/wooteco/subway/web/prehandler/validator/EmailMatchValidator.java new file mode 100644 index 000000000..3100418d5 --- /dev/null +++ b/src/main/java/wooteco/subway/web/prehandler/validator/EmailMatchValidator.java @@ -0,0 +1,24 @@ +package wooteco.subway.web.prehandler.validator; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import org.springframework.stereotype.Component; + +import wooteco.subway.web.service.member.MemberService; + +@Component +public class EmailMatchValidator implements ConstraintValidator { + private final MemberService memberService; + + public EmailMatchValidator(MemberService memberService) { + this.memberService = memberService; + } + + public void initialize(DuplicateCheck constraint) { + } + + public boolean isValid(String email, ConstraintValidatorContext context) { + return !memberService.isExistEmail(email); + } +} diff --git a/src/main/java/wooteco/subway/web/prehandler/validator/PasswordMatch.java b/src/main/java/wooteco/subway/web/prehandler/validator/PasswordMatch.java new file mode 100644 index 000000000..0d0a3d5a8 --- /dev/null +++ b/src/main/java/wooteco/subway/web/prehandler/validator/PasswordMatch.java @@ -0,0 +1,25 @@ +package wooteco.subway.web.prehandler.validator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +@Constraint(validatedBy = PasswordMatchValidator.class) +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PasswordMatch { + + String message() default ""; + + String field(); + + String fieldMatch(); + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/wooteco/subway/web/prehandler/validator/PasswordMatchValidator.java b/src/main/java/wooteco/subway/web/prehandler/validator/PasswordMatchValidator.java new file mode 100644 index 000000000..e1c67e4a6 --- /dev/null +++ b/src/main/java/wooteco/subway/web/prehandler/validator/PasswordMatchValidator.java @@ -0,0 +1,26 @@ +package wooteco.subway.web.prehandler.validator; + +import java.util.Objects; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import org.springframework.beans.BeanWrapperImpl; + +public class PasswordMatchValidator implements ConstraintValidator { + private String field; + private String fieldMatch; + + + public void initialize(PasswordMatch constraint) { + this.field = constraint.field(); + this.fieldMatch = constraint.fieldMatch(); + } + + public boolean isValid(Object value, ConstraintValidatorContext context) { + Object fieldValue = new BeanWrapperImpl(value).getPropertyValue(field); + Object fieldMatchValue = new BeanWrapperImpl(value).getPropertyValue(fieldMatch); + + return Objects.equals(fieldValue, fieldMatchValue); + } +} diff --git a/src/main/java/wooteco/subway/web/service/favorite/FavoriteService.java b/src/main/java/wooteco/subway/web/service/favorite/FavoriteService.java new file mode 100644 index 000000000..7cd76b8bf --- /dev/null +++ b/src/main/java/wooteco/subway/web/service/favorite/FavoriteService.java @@ -0,0 +1,74 @@ +package wooteco.subway.web.service.favorite; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import wooteco.subway.domain.favorite.Favorite; +import wooteco.subway.domain.favorite.FavoriteRepository; +import wooteco.subway.domain.member.Member; +import wooteco.subway.domain.station.Station; +import wooteco.subway.service.station.StationService; +import wooteco.subway.web.exception.NotFoundFavoriteException; +import wooteco.subway.web.service.favorite.dto.FavoriteDetailResponse; +import wooteco.subway.web.service.favorite.dto.FavoriteRequest; +import wooteco.subway.web.service.favorite.dto.FavoriteResponse; +import wooteco.subway.web.service.member.MemberService; + +@Service +@Transactional +public class FavoriteService { + + private final MemberService memberService; + private final StationService stationService; + private final FavoriteRepository favoriteRepository; + + public FavoriteService(MemberService memberService, StationService stationService, + FavoriteRepository favoriteRepository) { + this.memberService = memberService; + this.stationService = stationService; + this.favoriteRepository = favoriteRepository; + } + + public FavoriteResponse create(Member member, FavoriteRequest request) { + Favorite favorite = getFavorite(member, request); + return FavoriteResponse.of(favoriteRepository.save(favorite)); + } + + public void delete(Member member, Long id) { + Favorite favorite = favoriteRepository.findByIdAndMemberId(id, member.getId()) + .orElseThrow(() -> new NotFoundFavoriteException(id)); + favoriteRepository.delete(favorite); + } + + private Favorite getFavorite(Member member, FavoriteRequest request) { + Long sourceId = stationService.findStationIdByName(request.getSourceName()); + Long targetId = stationService.findStationIdByName(request.getTargetName()); + + return Favorite.of(member.getId(), sourceId, targetId); + } + + public Set getAll(Member member) { + List favorites = favoriteRepository.findAllByMemberId(member.getId()); + Set ids = new HashSet<>(); + for (Favorite favorite : favorites) { + ids.add(favorite.getSourceId()); + ids.add(favorite.getTargetId()); + } + List stations = stationService.findAllById(ids); + Map idToName = stations.stream() + .collect(Collectors.toMap(Station::getId, Station::getName)); + + return favorites.stream() + .map(favorite -> new FavoriteDetailResponse(favorite.getId(), favorite.getMemberId(), + favorite.getSourceId(), favorite.getTargetId(), + idToName.get(favorite.getSourceId()), + idToName.get(favorite.getTargetId()))) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/java/wooteco/subway/web/service/favorite/dto/FavoriteDetailResponse.java b/src/main/java/wooteco/subway/web/service/favorite/dto/FavoriteDetailResponse.java new file mode 100644 index 000000000..31705fc82 --- /dev/null +++ b/src/main/java/wooteco/subway/web/service/favorite/dto/FavoriteDetailResponse.java @@ -0,0 +1,48 @@ +package wooteco.subway.web.service.favorite.dto; + +public class FavoriteDetailResponse { + private Long id; + private Long memberId; + private Long sourceId; + private Long targetId; + private String sourceName; + private String targetName; + + public FavoriteDetailResponse(Long id, Long memberId, Long sourceId, Long targetId, + String sourceName, String targetName) { + this.id = id; + this.memberId = memberId; + this.sourceId = sourceId; + this.targetId = targetId; + this.sourceName = sourceName; + this.targetName = targetName; + } + + public FavoriteResponse toSimple() { + return new FavoriteResponse(this.id, this.memberId, this.sourceId, this.targetId); + } + + public Long getId() { + return id; + } + + public Long getMemberId() { + return memberId; + } + + public Long getSourceId() { + return sourceId; + } + + public Long getTargetId() { + return targetId; + } + + public String getSourceName() { + return sourceName; + } + + public String getTargetName() { + return targetName; + } +} diff --git a/src/main/java/wooteco/subway/web/service/favorite/dto/FavoriteRequest.java b/src/main/java/wooteco/subway/web/service/favorite/dto/FavoriteRequest.java new file mode 100644 index 000000000..961b7efea --- /dev/null +++ b/src/main/java/wooteco/subway/web/service/favorite/dto/FavoriteRequest.java @@ -0,0 +1,28 @@ +package wooteco.subway.web.service.favorite.dto; + +import javax.validation.constraints.NotBlank; + +public class FavoriteRequest { + + @NotBlank + private String sourceName; + + @NotBlank + private String targetName; + + public FavoriteRequest() { + } + + public FavoriteRequest(String sourceName, String targetName) { + this.sourceName = sourceName; + this.targetName = targetName; + } + + public String getSourceName() { + return sourceName; + } + + public String getTargetName() { + return targetName; + } +} diff --git a/src/main/java/wooteco/subway/web/service/favorite/dto/FavoriteResponse.java b/src/main/java/wooteco/subway/web/service/favorite/dto/FavoriteResponse.java new file mode 100644 index 000000000..a2df34e0c --- /dev/null +++ b/src/main/java/wooteco/subway/web/service/favorite/dto/FavoriteResponse.java @@ -0,0 +1,49 @@ +package wooteco.subway.web.service.favorite.dto; + +import java.util.Set; +import java.util.stream.Collectors; + +import wooteco.subway.domain.favorite.Favorite; + +public class FavoriteResponse { + private Long id; + private Long memberId; + private Long sourceId; + private Long targetId; + + protected FavoriteResponse() { + } + + public FavoriteResponse(Long id, Long memberId, Long sourceId, Long targetId) { + this.id = id; + this.memberId = memberId; + this.sourceId = sourceId; + this.targetId = targetId; + } + + public static FavoriteResponse of(Favorite favorite) { + return new FavoriteResponse(favorite.getId(), favorite.getMemberId(), + favorite.getSourceId(), + favorite.getTargetId()); + } + + public static Set setOf(Set favorites) { + return favorites.stream().map(FavoriteResponse::of).collect(Collectors.toSet()); + } + + public Long getId() { + return id; + } + + public Long getMemberId() { + return memberId; + } + + public Long getSourceId() { + return sourceId; + } + + public Long getTargetId() { + return targetId; + } +} diff --git a/src/main/java/wooteco/subway/web/service/member/MemberService.java b/src/main/java/wooteco/subway/web/service/member/MemberService.java new file mode 100644 index 000000000..ce46df01b --- /dev/null +++ b/src/main/java/wooteco/subway/web/service/member/MemberService.java @@ -0,0 +1,67 @@ +package wooteco.subway.web.service.member; + +import java.util.Optional; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import wooteco.subway.domain.member.Member; +import wooteco.subway.domain.member.MemberRepository; +import wooteco.subway.infra.JwtTokenProvider; +import wooteco.subway.web.exception.NotFoundMemberException; +import wooteco.subway.web.exception.NotMatchPasswordException; +import wooteco.subway.web.service.member.dto.LoginRequest; +import wooteco.subway.web.service.member.dto.UpdateMemberRequest; + +@Service +@Transactional +public class MemberService { + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + + public MemberService(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider) { + this.memberRepository = memberRepository; + this.jwtTokenProvider = jwtTokenProvider; + } + + public Member save(Member member) { + return memberRepository.save(member); + } + + public void updateMember(Long id, UpdateMemberRequest param) { + Member member = memberRepository.findById(id) + .orElseThrow(() -> new NotFoundMemberException(param.getName())); + if (!member.checkPassword(param.getOldPassword())) { + throw new NotMatchPasswordException(); + } + member.update(param.getName(), param.getNewPassword()); + memberRepository.save(member); + } + + public void deleteMember(Long id) { + memberRepository.deleteById(id); + } + + @Transactional(readOnly = true) + public String createToken(LoginRequest param) { + Member member = memberRepository.findByEmail(param.getEmail()) + .orElseThrow(() -> new NotFoundMemberException(param.getEmail())); + if (!member.checkPassword(param.getPassword())) { + throw new NotMatchPasswordException(); + } + + return jwtTokenProvider.createToken(param.getEmail()); + } + + @Transactional(readOnly = true) + public Member findMemberByEmail(String email) { + return memberRepository.findByEmail(email) + .orElseThrow(() -> new NotFoundMemberException(email)); + } + + @Transactional(readOnly = true) + public boolean isExistEmail(String email) { + Optional byEmail = memberRepository.findByEmail(email); + return byEmail.isPresent(); + } +} diff --git a/src/main/java/wooteco/subway/service/member/dto/LoginRequest.java b/src/main/java/wooteco/subway/web/service/member/dto/LoginRequest.java similarity index 88% rename from src/main/java/wooteco/subway/service/member/dto/LoginRequest.java rename to src/main/java/wooteco/subway/web/service/member/dto/LoginRequest.java index ccfedd688..7e6f501ea 100644 --- a/src/main/java/wooteco/subway/service/member/dto/LoginRequest.java +++ b/src/main/java/wooteco/subway/web/service/member/dto/LoginRequest.java @@ -1,4 +1,4 @@ -package wooteco.subway.service.member.dto; +package wooteco.subway.web.service.member.dto; public class LoginRequest { private String email; diff --git a/src/main/java/wooteco/subway/web/service/member/dto/MemberDetailResponse.java b/src/main/java/wooteco/subway/web/service/member/dto/MemberDetailResponse.java new file mode 100644 index 000000000..e35490d43 --- /dev/null +++ b/src/main/java/wooteco/subway/web/service/member/dto/MemberDetailResponse.java @@ -0,0 +1,50 @@ +package wooteco.subway.web.service.member.dto; + +import java.util.Set; +import java.util.stream.Collectors; + +import wooteco.subway.domain.member.Member; +import wooteco.subway.web.service.favorite.dto.FavoriteDetailResponse; +import wooteco.subway.web.service.favorite.dto.FavoriteResponse; + +public class MemberDetailResponse { + private Long id; + private String email; + private String name; + private Set favorites; + + public MemberDetailResponse() { + } + + public MemberDetailResponse(Long id, String email, String name, + Set favorites) { + this.id = id; + this.email = email; + this.name = name; + this.favorites = favorites; + } + + public static MemberDetailResponse of(Member member, Set favorites) { + Set simpleResponses = favorites.stream() + .map(FavoriteDetailResponse::toSimple) + .collect(Collectors.toSet()); + return new MemberDetailResponse(member.getId(), member.getEmail(), member.getName(), + simpleResponses); + } + + public Long getId() { + return id; + } + + public String getEmail() { + return email; + } + + public String getName() { + return name; + } + + public Set getFavorites() { + return favorites; + } +} diff --git a/src/main/java/wooteco/subway/web/service/member/dto/MemberRequest.java b/src/main/java/wooteco/subway/web/service/member/dto/MemberRequest.java new file mode 100644 index 000000000..27ac2f073 --- /dev/null +++ b/src/main/java/wooteco/subway/web/service/member/dto/MemberRequest.java @@ -0,0 +1,57 @@ +package wooteco.subway.web.service.member.dto; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; + +import wooteco.subway.domain.member.Member; +import wooteco.subway.web.prehandler.validator.DuplicateCheck; +import wooteco.subway.web.prehandler.validator.PasswordMatch; + +@PasswordMatch( + field = "password", + fieldMatch = "passwordCheck" +) +public class MemberRequest { + + @Email @DuplicateCheck @NotBlank(message = "이메일은 필수 입력 사항입니다.") + private String email; + + @NotBlank(message = "이름은 필수 입력 사항입니다.") + private String name; + + @NotBlank(message = "이름은 필수 입력 사항입니다.") + private String password; + + @NotBlank(message = "이름은 필수 입력 사항입니다.") + private String passwordCheck; + + public MemberRequest() { + } + + public MemberRequest(String email, String name, String password, String passwordCheck) { + this.email = email; + this.name = name; + this.password = password; + this.passwordCheck = passwordCheck; + } + + public Member toMember() { + return new Member(email, name, password); + } + + public String getEmail() { + return email; + } + + public String getName() { + return name; + } + + public String getPassword() { + return password; + } + + public String getPasswordCheck() { + return passwordCheck; + } +} diff --git a/src/main/java/wooteco/subway/service/member/dto/MemberResponse.java b/src/main/java/wooteco/subway/web/service/member/dto/MemberResponse.java similarity index 87% rename from src/main/java/wooteco/subway/service/member/dto/MemberResponse.java rename to src/main/java/wooteco/subway/web/service/member/dto/MemberResponse.java index ffad82ee2..9900384f1 100644 --- a/src/main/java/wooteco/subway/service/member/dto/MemberResponse.java +++ b/src/main/java/wooteco/subway/web/service/member/dto/MemberResponse.java @@ -1,4 +1,4 @@ -package wooteco.subway.service.member.dto; +package wooteco.subway.web.service.member.dto; import wooteco.subway.domain.member.Member; @@ -7,6 +7,9 @@ public class MemberResponse { private String email; private String name; + public MemberResponse() { + } + public MemberResponse(Long id, String email, String name) { this.id = id; this.email = email; diff --git a/src/main/java/wooteco/subway/service/member/dto/TokenResponse.java b/src/main/java/wooteco/subway/web/service/member/dto/TokenResponse.java similarity index 89% rename from src/main/java/wooteco/subway/service/member/dto/TokenResponse.java rename to src/main/java/wooteco/subway/web/service/member/dto/TokenResponse.java index 052ea785b..9274b159d 100644 --- a/src/main/java/wooteco/subway/service/member/dto/TokenResponse.java +++ b/src/main/java/wooteco/subway/web/service/member/dto/TokenResponse.java @@ -1,4 +1,4 @@ -package wooteco.subway.service.member.dto; +package wooteco.subway.web.service.member.dto; public class TokenResponse { private String accessToken; diff --git a/src/main/java/wooteco/subway/web/service/member/dto/UpdateMemberRequest.java b/src/main/java/wooteco/subway/web/service/member/dto/UpdateMemberRequest.java new file mode 100644 index 000000000..bc2a0f949 --- /dev/null +++ b/src/main/java/wooteco/subway/web/service/member/dto/UpdateMemberRequest.java @@ -0,0 +1,36 @@ +package wooteco.subway.web.service.member.dto; + +import javax.validation.constraints.NotBlank; + +public class UpdateMemberRequest { + + @NotBlank + private String name; + + @NotBlank + private String oldPassword; + + @NotBlank + private String newPassword; + + public UpdateMemberRequest() { + } + + public UpdateMemberRequest(String name, String oldPassword, String newPassword) { + this.name = name; + this.oldPassword = oldPassword; + this.newPassword = newPassword; + } + + public String getName() { + return name; + } + + public String getOldPassword() { + return oldPassword; + } + + public String getNewPassword() { + return newPassword; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 32f641014..75ddeac2c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,9 @@ logging.level.org.springframework.jdbc.core.JdbcTemplate=debug spring.h2.console.enabled=true +spring.datasource.data=classpath:data.sql handlebars.suffix=.html - -security.jwt.token.secret-key= secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret -security.jwt.token.expire-length= 3600000 +security.jwt.token.secret-key=secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret +security.jwt.token.expire-length=3600000 +logging.level.org.springframework.web=DEBUG +logging.level.root=debug +spring.output.ansi.enabled=detect \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 000000000..b4c36e683 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,31 @@ +INSERT INTO member (email, name, password) +VALUES ('dd@email.com', 'dd', 123); + +insert into station (name) +VALUES ('잠실'), + ('잠실새내'), + ('종합운동장'), + ('삼전'), + ('석촌고분'), + ('석촌'), + ('부산'), + ('대구'); + +insert into line (name, start_time, end_time, interval_time) +VALUES ('2호선', current_time, current_time, 3), + ('9호선', current_time, current_time, 3), + ('8호선', current_time, current_time, 3), + ('ktx', current_time, current_time, 3); + +insert into line_station (line, station_id, pre_station_id, distance, duration) +VALUES (1, 1, null, 0, 0), + (1, 2, 1, 10, 1), + (1, 3, 2, 10, 1), + (2, 3, null, 0, 0), + (2, 4, 3, 10, 1), + (2, 5, 4, 1, 10), + (2, 6, 5, 1, 10), + (3, 1, null, 0, 0), + (3, 6, 1, 1, 10), + (4, 7, null, 10, 10), + (4, 8, 7, 10, 10); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index b74084067..d512800a1 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -38,4 +38,11 @@ create table if not exists MEMBER primary key(id) ); --- // TODO 즐겨찾기 테이블 스키마 추가 +create table if not exists FAVORITE +( + id bigint auto_increment not null unique, + member_id bigint not null, + source_id bigint not null, + target_id bigint not null, + primary key(id) +); \ No newline at end of file diff --git a/src/main/resources/static/service/api/index.js b/src/main/resources/static/service/api/index.js index 02a7f5731..31af89fef 100644 --- a/src/main/resources/static/service/api/index.js +++ b/src/main/resources/static/service/api/index.js @@ -1,7 +1,13 @@ const METHOD = { - PUT() { + PUT(data) { return { - method: 'PUT' + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + ...data + }) } }, DELETE() { @@ -41,9 +47,44 @@ const api = (() => { } } + const member = { + join(joinForm) { + return request(`/members`, METHOD.POST(joinForm)); + }, + login(loginForm) { + return request(`/login`, METHOD.POST(loginForm)); + }, + myPage(){ + return requestWithJsonData(`/me`); + }, + update(id, updateForm){ + return request(`/members/`+id, METHOD.PUT(updateForm)); + }, + delete(id){ + return request(`/members/`+id, METHOD.DELETE()); + } + } + + const favorite = { + create(favoritePath) { + return request(`/favorites`, METHOD.POST(favoritePath)); + }, + get(id) { + return requestWithJsonData(`/favorites/${id}`); + }, + getAll() { + return requestWithJsonData(`/me/favorites`); + }, + delete(id) { + return request(`/favorites/${id}`, METHOD.DELETE()); + } + }; + return { line, - path + path, + member, + favorite, } })() diff --git a/src/main/resources/static/service/js/App.js b/src/main/resources/static/service/js/App.js index 5b9ccf6d2..2d7eed02b 100644 --- a/src/main/resources/static/service/js/App.js +++ b/src/main/resources/static/service/js/App.js @@ -1,9 +1,18 @@ -import { initNavigation } from '../utils/templates.js' +import { initLoginNavigation, initNavigation } from '../utils/templates.js' function SubwayApp() { this.init = () => { - initNavigation() + if (getCookie()) { + initLoginNavigation(); + } else { + initNavigation(); + } } + + const getCookie = function () { + const value = document.cookie.match('(^|;) ?token=([^;]*)(;|$)'); + return value ? value[2] : null; + }; } const subwayApp = new SubwayApp() diff --git a/src/main/resources/static/service/js/views/Favorite.js b/src/main/resources/static/service/js/views/Favorite.js index 8368bc827..6b8bbf5e5 100644 --- a/src/main/resources/static/service/js/views/Favorite.js +++ b/src/main/resources/static/service/js/views/Favorite.js @@ -1,18 +1,49 @@ -import { edgeItemTemplate } from '../../utils/templates.js' -import { defaultFavorites } from '../../utils/subwayMockData.js' +import { edgeItemTemplate } from "../../utils/templates.js"; +import api from "../../api/index.js"; +import { ERROR_MESSAGE, EVENT_TYPE, SUCCESS_MESSAGE } from "../../utils/constants.js"; function Favorite() { - const $favoriteList = document.querySelector('#favorite-list') + const $favoriteList = document.querySelector("#favorite-list"); - const loadFavoriteList = () => { - const template = defaultFavorites.map(edge => edgeItemTemplate(edge)).join('') - $favoriteList.insertAdjacentHTML('beforeend', template) - } + const initFavoriteList = async () => { + try { + const template = await api.favorite + .getAll() + .then(favorites => favorites.map(edge => edgeItemTemplate(edge)).join("")); + $favoriteList.innerHTML = template; + } + catch (e) { + alert(ERROR_MESSAGE.COMMON) + } + + }; + + const onDeleteHandler = async event => { + const $target = event.target; + const isDeleteButton = $target.classList.contains("mdi-delete"); + if (!isDeleteButton) { + return; + } + try { + const edgeId = $target.closest(".edge-item").dataset.edgeId; + await api.favorite.delete(edgeId); + await initFavoriteList(); + alert(SUCCESS_MESSAGE.COMMON); + } + catch (e) { + alert(ERROR_MESSAGE.COMMON); + } + }; + + const initEventListener = () => { + $favoriteList.addEventListener(EVENT_TYPE.CLICK, onDeleteHandler); + }; this.init = () => { - loadFavoriteList() - } + initFavoriteList(); + initEventListener(); + }; } -const favorite = new Favorite() -favorite.init() +const favorite = new Favorite(); +favorite.init(); diff --git a/src/main/resources/static/service/js/views/Join.js b/src/main/resources/static/service/js/views/Join.js new file mode 100644 index 000000000..819e7b2cc --- /dev/null +++ b/src/main/resources/static/service/js/views/Join.js @@ -0,0 +1,45 @@ +import { EVENT_TYPE } from '../../utils/constants.js' +import api from '../../api/index.js' + +function Join() { + const $email = document.querySelector('#email'); + const $name = document.querySelector('#name'); + const $password = document.querySelector('#password'); + const $passwordCheck = document.querySelector('#password-check'); + const $joinButton = document.querySelector('#join-button'); + + const onClickJoinButton = event => { + event.preventDefault(); + + const joinForm = { + email: $email.value, + name: $name.value, + password: $password.value, + passwordCheck: $passwordCheck.value, + }; + + api.member.join(joinForm) + .then(response => { + if (!response.ok) { + throw response; + } + window.location = "/login"; + }).catch(response => response.json()) + .then(errorResponse => { + if (errorResponse) { + alert(errorResponse.message); + } + }); + } + + const initEventListener = () => { + $joinButton.addEventListener(EVENT_TYPE.CLICK, onClickJoinButton) + } + + this.init = () => { + initEventListener(); + } +} + +const join = new Join(); +join.init(); diff --git a/src/main/resources/static/service/js/views/Login.js b/src/main/resources/static/service/js/views/Login.js index 549242697..c58b38d35 100644 --- a/src/main/resources/static/service/js/views/Login.js +++ b/src/main/resources/static/service/js/views/Login.js @@ -1,21 +1,50 @@ -import { EVENT_TYPE, ERROR_MESSAGE } from '../../utils/constants.js' +import { ERROR_MESSAGE, EVENT_TYPE } from '../../utils/constants.js' +import api from '../../api/index.js'; +import { setCookie } from '../../utils/loginUtils.js'; function Login() { - const $loginButton = document.querySelector('#login-button') + const $loginButton = document.querySelector('#login-button'); + const $email = document.querySelector('#email'); + const $password = document.querySelector('#password'); + const onLogin = event => { - event.preventDefault() - const emailValue = document.querySelector('#email').value - const passwordValue = document.querySelector('#password').value - if (!emailValue && !passwordValue) { - Snackbar.show({ text: ERROR_MESSAGE.LOGIN_FAIL, pos: 'bottom-center', showAction: false, duration: 2000 }) - return + event.preventDefault(); + const emailValue = $email.value; + const passwordValue = $password.value; + const data = { + email: emailValue, + password: passwordValue, + }; + if (!emailValue || !passwordValue) { + Snackbar.show({ + text: ERROR_MESSAGE.LOGIN_FAIL, + pos: 'bottom-center', + showAction: false, + duration: 2000 + }); + return; } + api.member.login(data) + .then(response => { + if (!response.ok) { + throw response; + } + return response.json(); + }).then(tokenResponse => { + setCookie(tokenResponse.accessToken); + window.location = "/my-page"; + }).catch(error => error.json()) + .then(errorResponse => { + if (errorResponse) { + alert(errorResponse.message); + } + }); } this.init = () => { - $loginButton.addEventListener(EVENT_TYPE.CLICK, onLogin) + $loginButton.addEventListener(EVENT_TYPE.CLICK, onLogin); } } -const login = new Login() -login.init() +const login = new Login(); +login.init(); diff --git a/src/main/resources/static/service/js/views/MyPage.js b/src/main/resources/static/service/js/views/MyPage.js new file mode 100644 index 000000000..25aa8154b --- /dev/null +++ b/src/main/resources/static/service/js/views/MyPage.js @@ -0,0 +1,23 @@ +import api from '../../api/index.js'; +import { getCookie } from '../../utils/loginUtils.js'; + +function MyPage() { + + this.init = () => { + const $email = document.querySelector("#email"); + const $name = document.querySelector("#name"); + + if(getCookie()) { + api.member.myPage().then(response => { + $email.innerText = response.email; + $name.innerText = response.name; + }); + } else{ + alert("로그인을 해주세요"); + window.location = "/login"; + } + } +} + +const myPage = new MyPage(); +myPage.init(); diff --git a/src/main/resources/static/service/js/views/MyPageEdit.js b/src/main/resources/static/service/js/views/MyPageEdit.js new file mode 100644 index 000000000..cec4a4167 --- /dev/null +++ b/src/main/resources/static/service/js/views/MyPageEdit.js @@ -0,0 +1,87 @@ +import { EVENT_TYPE } from '../../utils/constants.js' +import api from '../../api/index.js' +import { deleteCookie, getCookie } from '../../utils/loginUtils.js'; + +function MyPageEdit() { + const $id = document.querySelector('#id'); + const $email = document.querySelector('#email'); + const $name = document.querySelector('#name'); + const $oldPassword = document.querySelector('#old-password'); + const $password = document.querySelector('#password'); + const $passwordCheck = document.querySelector('#password-check'); + const $updateButton = document.querySelector('#update-button'); + const $signOutButton = document.querySelector('#sign-out-button'); + + const onClickUpdateButton = event => { + event.preventDefault(); + + const updateForm = { + name: $name.value, + oldPassword: $oldPassword.value, + newPassword: $password.value, + }; + + if ($password.value !== $passwordCheck.value) { + alert("패스워드가 일치하지 않습니다. 😡"); + return; + } + + api.member.update($id.dataset.id, updateForm) + .then(response => { + if (!response.ok) { + throw response; + } + window.location = "/my-page"; + }).catch(response => response.json()) + .then(errorResponse => { + if (errorResponse) { + alert(errorResponse.message); + } + }); + } + + const onClickSignOutButton = event => { + event.preventDefault(); + + if (!confirm("정말로 회원 탈퇴를 하실겁니까? 😳")) { + window.location = "/my-page" + return; + } + api.member.delete($id.dataset.id, $oldPassword.value) + .then(response => { + if(!response.ok){ + throw response; + } + alert("실망이야..😢"); + deleteCookie(); + window.location = "/login" + }).catch(response => response.json()) + .then(error => { + if (error) { + alert(error.message); + } + }); + } + + const initEventListener = () => { + $updateButton.addEventListener(EVENT_TYPE.CLICK, onClickUpdateButton); + $signOutButton.addEventListener(EVENT_TYPE.CLICK, onClickSignOutButton); + } + + this.init = () => { + if(getCookie()) { + api.member.myPage().then(response => { + $id.dataset.id = response.id; + $email.value = response.email; + $name.value = response.name; + }).catch(alert); + initEventListener(); + } else{ + alert("로그인을 해주세요"); + window.location = '/login'; + } + } +} + +const myPageEdit = new MyPageEdit(); +myPageEdit.init(); diff --git a/src/main/resources/static/service/js/views/Search.js b/src/main/resources/static/service/js/views/Search.js index 45d42b747..dedc9cf94 100644 --- a/src/main/resources/static/service/js/views/Search.js +++ b/src/main/resources/static/service/js/views/Search.js @@ -1,7 +1,7 @@ -import { EVENT_TYPE } from '../../utils/constants.js' +import { ERROR_MESSAGE, EVENT_TYPE, PATH_TYPE } from '../../utils/constants.js' import api from '../../api/index.js' import { searchResultTemplate } from '../../utils/templates.js' -import { PATH_TYPE, ERROR_MESSAGE } from '../../utils/constants.js' +import { getCookie } from '../../utils/loginUtils.js'; function Search() { const $departureStationName = document.querySelector('#departure-station-name') @@ -49,7 +49,32 @@ function Search() { const onToggleFavorite = event => { event.preventDefault() - const isFavorite = $favoriteButton.classList.contains('mdi-star') + + if (!getCookie()) { + alert("로그인 먼저 해주세요."); + window.location="/login"; + return; + } + + const data = { + sourceName: $departureStationName.value, + targetName: $arrivalStationName.value + } + + api.favorite.create(data) + .then(response => { + if (!response.ok) { + throw response; + } + alert("등록되었습니다."); + }).catch(response => response.json()) + .then(error => { + if (error) { + alert(error.message); + return; + } + }); + const isFavorite = $favoriteButton.classList.contains('mdi-star'); const classList = $favoriteButton.classList if (isFavorite) { diff --git a/src/main/resources/static/service/lib/snackbar/index.js b/src/main/resources/static/service/lib/snackbar/index.js new file mode 100644 index 000000000..20c3070f7 --- /dev/null +++ b/src/main/resources/static/service/lib/snackbar/index.js @@ -0,0 +1,10 @@ +import Snackbar from "./snackbar.js"; + +export default function showSnackbar(message) { + Snackbar.show({ + text: message, + pos: "bottom-center", + showAction: false, + duration: 2000 + }); +} diff --git a/src/main/resources/static/service/lib/snackbar/snackbar.css b/src/main/resources/static/service/lib/snackbar/snackbar.css new file mode 100644 index 000000000..ac0857802 --- /dev/null +++ b/src/main/resources/static/service/lib/snackbar/snackbar.css @@ -0,0 +1,99 @@ +.snackbar-container { + transition: all 500ms ease; + transition-property: top, right, bottom, left, opacity; + font-family: Roboto, sans-serif; + font-size: 14px; + min-height: 14px; + background-color: #070b0e; + position: fixed; + display: flex; + justify-content: space-between; + align-items: center; + color: white; + line-height: 22px; + padding: 18px 24px; + bottom: -100px; + top: -100px; + opacity: 0; + z-index: 9999; +} + +.snackbar-container p { + width: 100%; + text-align: center; +} + +.snackbar-container .action { + background: inherit; + display: inline-block; + border: none; + font-size: inherit; + text-transform: uppercase; + color: #4caf50; + padding: 0; + min-width: min-content; + cursor: pointer; +} + +@media (min-width: 640px) { + .snackbar-container { + min-width: 288px; + max-width: 568px; + display: inline-flex; + border-radius: 2px; + } +} + +@media (max-width: 640px) { + .snackbar-container { + left: 0; + right: 0; + width: 100%; + } +} + +.snackbar-pos.bottom-center { + top: auto !important; + bottom: 0; + left: 50%; + transform: translate(-50%, 0); +} + +.snackbar-pos.bottom-left { + top: auto !important; + bottom: 0; + left: 0; +} + +.snackbar-pos.bottom-right { + top: auto !important; + bottom: 0; + right: 0; +} + +.snackbar-pos.top-left { + bottom: auto !important; + top: 0; + left: 0; +} + +.snackbar-pos.top-center { + bottom: auto !important; + top: 0; + left: 50%; + transform: translate(-50%, 0); +} + +.snackbar-pos.top-right { + bottom: auto !important; + top: 0; + right: 0; +} + +@media (max-width: 640px) { + .snackbar-pos.bottom-center, + .snackbar-pos.top-center { + left: 0; + transform: none; + } +} diff --git a/src/main/resources/static/service/lib/snackbar/snackbar.js b/src/main/resources/static/service/lib/snackbar/snackbar.js new file mode 100644 index 000000000..029c9769f --- /dev/null +++ b/src/main/resources/static/service/lib/snackbar/snackbar.js @@ -0,0 +1,183 @@ +/*! + * Snackbar v0.1.11 + * http://polonel.com/Snackbar + * + * Copyright 2018 Chris Brame and other contributors + * Released under the MIT license + * https://github.com/polonel/Snackbar/blob/master/LICENSE + */ + +(function(root, factory) { + "use strict"; + + if (typeof define === "function" && define.amd) { + define([], function() { + return (root.Snackbar = factory()); + }); + } else if (typeof module === "object" && module.exports) { + module.exports = root.Snackbar = factory(); + } else { + root.Snackbar = factory(); + } +})(this, function() { + var Snackbar = {}; + + Snackbar.current = null; + var $defaults = { + text: "Default Text", + textColor: "#FFFFFF", + width: "auto", + showAction: true, + actionText: "Dismiss", + actionTextColor: "#4CAF50", + showSecondButton: false, + secondButtonText: "", + secondButtonTextColor: "#4CAF50", + backgroundColor: "#323232", + pos: "bottom-left", + duration: 5000, + customClass: "", + onActionClick: function(element) { + element.style.opacity = 0; + }, + onSecondButtonClick: function(element) {}, + onClose: function(element) {} + }; + + Snackbar.show = function($options) { + var options = Extend(true, $defaults, $options); + + if (Snackbar.current) { + Snackbar.current.style.opacity = 0; + setTimeout( + function() { + var $parent = this.parentElement; + if ($parent) + // possible null if too many/fast Snackbars + $parent.removeChild(this); + }.bind(Snackbar.current), + 500 + ); + } + + Snackbar.snackbar = document.createElement("div"); + Snackbar.snackbar.className = "snackbar-container " + options.customClass; + Snackbar.snackbar.style.width = options.width; + var $p = document.createElement("p"); + $p.style.margin = 0; + $p.style.padding = 0; + $p.style.color = options.textColor; + $p.style.fontSize = "14px"; + $p.style.fontWeight = 300; + $p.style.lineHeight = "1em"; + $p.innerHTML = options.text; + Snackbar.snackbar.appendChild($p); + Snackbar.snackbar.style.background = options.backgroundColor; + + if (options.showSecondButton) { + var secondButton = document.createElement("button"); + secondButton.className = "action"; + secondButton.innerHTML = options.secondButtonText; + secondButton.style.color = options.secondButtonTextColor; + secondButton.addEventListener("click", function() { + options.onSecondButtonClick(Snackbar.snackbar); + }); + Snackbar.snackbar.appendChild(secondButton); + } + + if (options.showAction) { + var actionButton = document.createElement("button"); + actionButton.className = "action"; + actionButton.innerHTML = options.actionText; + actionButton.style.color = options.actionTextColor; + actionButton.addEventListener("click", function() { + options.onActionClick(Snackbar.snackbar); + }); + Snackbar.snackbar.appendChild(actionButton); + } + + if (options.duration) { + setTimeout( + function() { + if (Snackbar.current === this) { + Snackbar.current.style.opacity = 0; + // When natural remove event occurs let's move the snackbar to its origins + Snackbar.current.style.top = "-100px"; + Snackbar.current.style.bottom = "-100px"; + } + }.bind(Snackbar.snackbar), + options.duration + ); + } + + Snackbar.snackbar.addEventListener( + "transitionend", + function(event, elapsed) { + if (event.propertyName === "opacity" && this.style.opacity === "0") { + if (typeof options.onClose === "function") options.onClose(this); + + this.parentElement.removeChild(this); + if (Snackbar.current === this) { + Snackbar.current = null; + } + } + }.bind(Snackbar.snackbar) + ); + + Snackbar.current = Snackbar.snackbar; + + document.body.appendChild(Snackbar.snackbar); + var $bottom = getComputedStyle(Snackbar.snackbar).bottom; + var $top = getComputedStyle(Snackbar.snackbar).top; + Snackbar.snackbar.style.opacity = 1; + Snackbar.snackbar.className = + "snackbar-container " + + options.customClass + + " snackbar-pos " + + options.pos; + }; + + Snackbar.close = function() { + if (Snackbar.current) { + Snackbar.current.style.opacity = 0; + } + }; + + // Pure JS Extend + // http://gomakethings.com/vanilla-javascript-version-of-jquery-extend/ + var Extend = function() { + var extended = {}; + var deep = false; + var i = 0; + var length = arguments.length; + + if (Object.prototype.toString.call(arguments[0]) === "[object Boolean]") { + deep = arguments[0]; + i++; + } + + var merge = function(obj) { + for (var prop in obj) { + if (Object.prototype.hasOwnProperty.call(obj, prop)) { + if ( + deep && + Object.prototype.toString.call(obj[prop]) === "[object Object]" + ) { + extended[prop] = Extend(true, extended[prop], obj[prop]); + } else { + extended[prop] = obj[prop]; + } + } + } + }; + + for (; i < length; i++) { + var obj = arguments[i]; + merge(obj); + } + + return extended; + }; + + return Snackbar; +}); diff --git a/src/main/resources/static/service/utils/constants.js b/src/main/resources/static/service/utils/constants.js index 945237b4b..2d39fc49d 100644 --- a/src/main/resources/static/service/utils/constants.js +++ b/src/main/resources/static/service/utils/constants.js @@ -1,12 +1,22 @@ export const EVENT_TYPE = { - CLICK: 'click' -} + CLICK: "click", + KEY_PRESS: "keypress" +}; + +export const SUCCESS_MESSAGE = { + COMMON: "😁 성공적으로 변경되었습니다.", + SAVE: "😁 정보가 반영되었습니다..", + FAVORITE: "😁 즐겨찾기에 추가하였습니다." +}; export const ERROR_MESSAGE = { - LOGIN_FAIL: '😭 로그인이 실패했습니다. 다시 시도해주세요.' -} + COMMON: "🕵️‍♂️ 에러가 발생했습니다.", + PASSWORD_CHECK: "🕵️‍♂️ 패스워드가 일치하지 않습니다.", + LOGIN_FAIL: "😭 로그인이 실패했습니다. 다시 시도해주세요.", + JOIN_FAIL: "😭 회원가입이 실패했습니다. " +}; export const PATH_TYPE = { - DISTANCE: 'DISTANCE', - DURATION: 'DURATION' -} + DISTANCE: "DISTANCE", + DURATION: "DURATION" +}; \ No newline at end of file diff --git a/src/main/resources/static/service/utils/loginUtils.js b/src/main/resources/static/service/utils/loginUtils.js new file mode 100644 index 000000000..16fb0a4c2 --- /dev/null +++ b/src/main/resources/static/service/utils/loginUtils.js @@ -0,0 +1,15 @@ +export const getCookie = function () { + const value = document.cookie.match('(^|;) ?token=([^;]*)(;|$)'); + return value ? value[2] : null; +}; + +export const deleteCookie = function () { + document.cookie = 'token=; expires=Thu, 01 Jan 1999 00:00:10 GMT;'; +} + +export const setCookie = function (value) { + const date = new Date(); + date.setTime(date.getTime() + 5 * 60 * 1000); + document.cookie = "token=" + value + ';expires=' + date.toUTCString(); +}; + diff --git a/src/main/resources/static/service/utils/templates.js b/src/main/resources/static/service/utils/templates.js index e22d5c7e7..19a2c8276 100644 --- a/src/main/resources/static/service/utils/templates.js +++ b/src/main/resources/static/service/utils/templates.js @@ -19,11 +19,57 @@ export const navTemplate = `` +export const navLoginTemplate = `` + + export const subwayLinesItemTemplate = line => { - const stations = line.stations ? line.stations.map(station => listItemTemplate(station)).join('') : null + const stations = line.stations ? line.stations.map(station => listItemTemplate(station)) + .join('') : null return `
${line.name}
@@ -36,7 +82,9 @@ export const subwayLinesItemTemplate = line => { export const searchResultTemplate = result => { const lastIndex = result.stations.length - 1 - const pathResultTemplate = result.stations.map((station, index) => pathStationTemplate(station.name, index, lastIndex)).join('') + const pathResultTemplate = result.stations.map((station, index) => pathStationTemplate(station.name, + index, + lastIndex)).join('') return `
@@ -73,3 +121,25 @@ export const pathStationTemplate = (name, index, lastIndex) => { export const initNavigation = () => { document.querySelector('body').insertAdjacentHTML('afterBegin', navTemplate) } + +export const initLoginNavigation = () => { + document.querySelector('body').insertAdjacentHTML('afterBegin', navLoginTemplate) +} + +export const edgeItemTemplate = edge => { + return `
  • + + ${ + edge.sourceName ? edge.sourceName : "출발역" + } + + ${ + edge.targetName ? edge.targetName : "도착역" + } + +
  • `; +}; diff --git a/src/main/resources/templates/service/favorite.html b/src/main/resources/templates/service/favorite.html index d05f1f655..47a788e2f 100644 --- a/src/main/resources/templates/service/favorite.html +++ b/src/main/resources/templates/service/favorite.html @@ -1,26 +1,43 @@ - - - RunningMap - - - - - - - -
    -
    -
    -
    즐겨찾기
    -
    -
      -
      -
      + + + RunningMap + + + + + + + + + +
      +
      +
      +
      즐겨찾기
      +
      +
        - - - +
        +
        + + + + diff --git a/src/main/resources/templates/service/index.html b/src/main/resources/templates/service/index.html index 373106caf..19cae15e1 100644 --- a/src/main/resources/templates/service/index.html +++ b/src/main/resources/templates/service/index.html @@ -25,7 +25,7 @@ 회원가입
      • - 마이페이지 + 마이페이지
      • 나의 정보 수정 @@ -37,6 +37,9 @@
      • 경로 검색
      • +
      • + 즐겨 찾기 +
      • diff --git a/src/main/resources/templates/service/join.html b/src/main/resources/templates/service/join.html index 4d8f59512..2a481f1f8 100644 --- a/src/main/resources/templates/service/join.html +++ b/src/main/resources/templates/service/join.html @@ -59,6 +59,7 @@
        + diff --git a/src/main/resources/templates/service/mypage-edit.html b/src/main/resources/templates/service/mypage-edit.html index ad4515458..f3f057a4b 100644 --- a/src/main/resources/templates/service/mypage-edit.html +++ b/src/main/resources/templates/service/mypage-edit.html @@ -13,6 +13,7 @@
        나의 정보 수정
        +
        @@ -33,43 +36,57 @@ id="name" type="text" placeholder="name" + value="" + /> +
        +
        + +
        저장 -
        + diff --git a/src/main/resources/templates/service/mypage.html b/src/main/resources/templates/service/mypage.html index 5e10d7091..0e1eaa13b 100644 --- a/src/main/resources/templates/service/mypage.html +++ b/src/main/resources/templates/service/mypage.html @@ -14,14 +14,14 @@
        나의 정보
        email
        -
        eastjun@woowahan.com
        +
        name
        -
        eastjun
        +
        @@ -33,5 +33,6 @@
        + diff --git a/src/main/resources/templates/service/search.html b/src/main/resources/templates/service/search.html index e0924309c..01702b2a3 100644 --- a/src/main/resources/templates/service/search.html +++ b/src/main/resources/templates/service/search.html @@ -8,6 +8,7 @@ +
        @@ -17,11 +18,11 @@
        @@ -30,11 +31,11 @@
        @@ -46,8 +47,8 @@