diff --git a/build.gradle b/build.gradle index 5199aa45..fa8b3640 100644 --- a/build.gradle +++ b/build.gradle @@ -76,6 +76,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + // oAuth2 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-authorization-server' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + // Session implementation 'org.springframework.session:spring-session-data-redis' diff --git a/keystore.p12 b/keystore.p12 new file mode 100644 index 00000000..c70b283a Binary files /dev/null and b/keystore.p12 differ diff --git a/src/main/java/org/programmers/signalbuddy/domain/admin/controller/AdminWebController.java b/src/main/java/org/programmers/signalbuddy/domain/admin/controller/AdminWebController.java index 3cd5d409..8b5e76be 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/admin/controller/AdminWebController.java +++ b/src/main/java/org/programmers/signalbuddy/domain/admin/controller/AdminWebController.java @@ -1,6 +1,7 @@ package org.programmers.signalbuddy.domain.admin.controller; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.programmers.signalbuddy.domain.admin.dto.AdminMemberResponse; import org.programmers.signalbuddy.domain.admin.dto.WithdrawalMemberResponse; import org.programmers.signalbuddy.domain.admin.service.AdminService; @@ -13,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; +@Slf4j @Controller @RequiredArgsConstructor @RequestMapping("/admins") @@ -20,6 +22,11 @@ public class AdminWebController { private final AdminService adminService; + @GetMapping() + public ModelAndView adminsMain() { + return new ModelAndView("admin/main"); + } + @GetMapping("members/list") public ModelAndView getAllMembers(@PageableDefault(page = 0, size = 10, sort = "email") Pageable pageable, ModelAndView mv) { Page members = adminService.getAllMembers(pageable); @@ -32,7 +39,7 @@ public ModelAndView getAllMembers(@PageableDefault(page = 0, size = 10, sort = " public ModelAndView getMember(@PathVariable Long id, ModelAndView mv) { final AdminMemberResponse member = adminService.getMember(id); mv.setViewName("admin/detail"); - mv.addObject("member", member); + mv.addObject("m", member); return mv; } diff --git a/src/main/java/org/programmers/signalbuddy/domain/admin/service/AdminService.java b/src/main/java/org/programmers/signalbuddy/domain/admin/service/AdminService.java index 44a6f77d..678129d2 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/admin/service/AdminService.java +++ b/src/main/java/org/programmers/signalbuddy/domain/admin/service/AdminService.java @@ -33,7 +33,7 @@ public class AdminService { private BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); public Page getAllMembers(Pageable pageable) { - Page membersPage = memberRepository.findAll(pageable); + Page membersPage = memberRepository.findAllMembers(pageable); Page adminMemberResponses = membersPage.map(member -> { diff --git a/src/main/java/org/programmers/signalbuddy/domain/basetime/BaseTimeEntity.java b/src/main/java/org/programmers/signalbuddy/domain/basetime/BaseTimeEntity.java index 7b55d09a..0e8444a9 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/basetime/BaseTimeEntity.java +++ b/src/main/java/org/programmers/signalbuddy/domain/basetime/BaseTimeEntity.java @@ -19,4 +19,5 @@ public abstract class BaseTimeEntity { @LastModifiedDate private LocalDateTime updatedAt; + } \ No newline at end of file diff --git a/src/main/java/org/programmers/signalbuddy/domain/bookmark/controller/BookmarkController.java b/src/main/java/org/programmers/signalbuddy/domain/bookmark/controller/BookmarkController.java index 1649acbf..de5403ca 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/bookmark/controller/BookmarkController.java +++ b/src/main/java/org/programmers/signalbuddy/domain/bookmark/controller/BookmarkController.java @@ -8,6 +8,8 @@ import org.programmers.signalbuddy.domain.bookmark.dto.BookmarkRequest; import org.programmers.signalbuddy.domain.bookmark.dto.BookmarkResponse; import org.programmers.signalbuddy.domain.bookmark.service.BookmarkService; +import org.programmers.signalbuddy.global.annotation.CurrentUser; +import org.programmers.signalbuddy.global.dto.CustomUser2Member; import org.springframework.boot.autoconfigure.security.SecurityProperties.User; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -46,7 +48,7 @@ public ResponseEntity> getBookmarks( @PostMapping @ApiResponse(responseCode = "201", description = "즐겨찾기 등록 성공") public ResponseEntity addBookmark( - @RequestBody @Validated BookmarkRequest createRequest, User user) { + @RequestBody @Validated BookmarkRequest createRequest, @CurrentUser CustomUser2Member user) { log.info("createRequest: {}", createRequest); final BookmarkResponse created = bookmarkService.createBookmark(createRequest, user); return ResponseEntity.status(HttpStatus.CREATED).body(created); @@ -56,7 +58,7 @@ public ResponseEntity addBookmark( @PatchMapping("{id}") @ApiResponse(responseCode = "200", description = "즐겨찾기 수정 성공") public ResponseEntity updateBookmark( - @RequestBody @Validated BookmarkRequest updateRequest, @PathVariable Long id, User user) { + @RequestBody @Validated BookmarkRequest updateRequest, @PathVariable Long id, @CurrentUser CustomUser2Member user) { log.info("updateRequest: {}", updateRequest); final BookmarkResponse updated = bookmarkService.updateBookmark(updateRequest, id, user); return ResponseEntity.ok().body(updated); @@ -65,8 +67,7 @@ public ResponseEntity updateBookmark( @Operation(summary = "즐겨찾기 삭제", description = "즐겨찾기 삭제 기능") @DeleteMapping("{id}") @ApiResponse(responseCode = "200", description = "즐겨찾기 삭제 성공") - public ResponseEntity deleteBookmark(@PathVariable Long id, User user) { - user.setName("1"); + public ResponseEntity deleteBookmark(@PathVariable Long id, @CurrentUser CustomUser2Member user) { log.info("delete bookmark id: {}", id); bookmarkService.deleteBookmark(id, user); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); diff --git a/src/main/java/org/programmers/signalbuddy/domain/bookmark/controller/BookmarkWebController.java b/src/main/java/org/programmers/signalbuddy/domain/bookmark/controller/BookmarkWebController.java index 47ad6c72..82bc78fc 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/bookmark/controller/BookmarkWebController.java +++ b/src/main/java/org/programmers/signalbuddy/domain/bookmark/controller/BookmarkWebController.java @@ -3,12 +3,11 @@ import lombok.RequiredArgsConstructor; import org.programmers.signalbuddy.domain.bookmark.dto.BookmarkResponse; import org.programmers.signalbuddy.domain.bookmark.service.BookmarkService; -import org.springframework.boot.autoconfigure.security.SecurityProperties.User; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; +import org.programmers.signalbuddy.global.annotation.CurrentUser; +import org.programmers.signalbuddy.global.dto.CustomUser2Member; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; @@ -20,6 +19,11 @@ public class BookmarkWebController { private final BookmarkService bookmarkService; + @ModelAttribute("user") + public CustomUser2Member currentUser(@CurrentUser CustomUser2Member user) { + return user; + } + @GetMapping("{id}") public ModelAndView getBookmark(@PathVariable Long id, ModelAndView mv) { final BookmarkResponse bookmark = bookmarkService.getBookmark(id); diff --git a/src/main/java/org/programmers/signalbuddy/domain/bookmark/repository/BookmarkRepositoryCustomImpl.java b/src/main/java/org/programmers/signalbuddy/domain/bookmark/repository/BookmarkRepositoryCustomImpl.java index bf1a5005..0bc7020c 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/bookmark/repository/BookmarkRepositoryCustomImpl.java +++ b/src/main/java/org/programmers/signalbuddy/domain/bookmark/repository/BookmarkRepositoryCustomImpl.java @@ -37,12 +37,12 @@ public class BookmarkRepositoryCustomImpl implements BookmarkRepositoryCustom { public Page findPagedByMember(Pageable pageable, Long memberId) { final List responses = queryFactory.select(pageBookmarkDto).from(bookmark) .join(member) - .on(bookmark.member.eq(member).and(member.memberId.eq(1L))) // TODO : 1L -> memberId + .on(bookmark.member.eq(member).and(member.memberId.eq(memberId))) .offset(pageable.getOffset()).limit(pageable.getPageSize()) .orderBy(new OrderSpecifier<>(Order.ASC, bookmark.bookmarkId)).fetch(); final Long count = queryFactory.select(bookmark.count()).from(bookmark).join(member) - .on(member.memberId.eq(1L)) // TODO : 1L -> memberId + .on(bookmark.member.eq(member).and(member.memberId.eq(memberId))) .fetchOne(); return new PageImpl<>(responses, pageable, count != null ? count : 0); } diff --git a/src/main/java/org/programmers/signalbuddy/domain/bookmark/service/BookmarkService.java b/src/main/java/org/programmers/signalbuddy/domain/bookmark/service/BookmarkService.java index 44728a92..bc7a99c8 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/bookmark/service/BookmarkService.java +++ b/src/main/java/org/programmers/signalbuddy/domain/bookmark/service/BookmarkService.java @@ -14,8 +14,8 @@ import org.programmers.signalbuddy.domain.member.entity.Member; import org.programmers.signalbuddy.domain.member.exception.MemberErrorCode; import org.programmers.signalbuddy.domain.member.repository.MemberRepository; +import org.programmers.signalbuddy.global.dto.CustomUser2Member; import org.programmers.signalbuddy.global.exception.BusinessException; -import org.springframework.boot.autoconfigure.security.SecurityProperties.User; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -34,7 +34,7 @@ public Page findPagedBookmarks(Pageable pageable, Long memberI return bookmarkRepository.findPagedByMember(pageable, memberId); } - public BookmarkResponse createBookmark(BookmarkRequest createRequest, User user) { + public BookmarkResponse createBookmark(BookmarkRequest createRequest, CustomUser2Member user) { final Member member = getMember(user); final Point point = toPoint(createRequest.getLng(), createRequest.getLat()); @@ -45,7 +45,8 @@ public BookmarkResponse createBookmark(BookmarkRequest createRequest, User user) } @Transactional - public BookmarkResponse updateBookmark(BookmarkRequest updateRequest, Long id, User user) { + public BookmarkResponse updateBookmark(BookmarkRequest updateRequest, Long id, + CustomUser2Member user) { final Member member = getMember(user); final Bookmark bookmark = bookmarkRepository.findById(id) @@ -58,15 +59,15 @@ public BookmarkResponse updateBookmark(BookmarkRequest updateRequest, Long id, U } @Transactional - public void deleteBookmark(Long id, User user) { + public void deleteBookmark(Long id, CustomUser2Member user) { final Member member = getMember(user); final Bookmark bookmark = bookmarkRepository.findById(id) .orElseThrow(() -> new BusinessException(BookmarkErrorCode.NOT_FOUND_BOOKMARK)); bookmarkRepository.delete(bookmark); } - private Member getMember(User user) { - return memberRepository.findById(Long.parseLong(user.getName())) // TODO : User 수정 필요 + private Member getMember(CustomUser2Member user) { + return memberRepository.findById(user.getMemberId()) .orElseThrow(() -> new BusinessException(MemberErrorCode.NOT_FOUND_MEMBER)); } diff --git a/src/main/java/org/programmers/signalbuddy/domain/crossroad/controller/CrossroadController.java b/src/main/java/org/programmers/signalbuddy/domain/crossroad/controller/CrossroadController.java index 5e26f63f..2e4fe0c7 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/crossroad/controller/CrossroadController.java +++ b/src/main/java/org/programmers/signalbuddy/domain/crossroad/controller/CrossroadController.java @@ -1,14 +1,18 @@ package org.programmers.signalbuddy.domain.crossroad.controller; +import org.programmers.signalbuddy.domain.crossroad.dto.CrossroadApiResponse; +import org.programmers.signalbuddy.domain.crossroad.dto.CrossroadStateApiResponse; +import org.programmers.signalbuddy.domain.crossroad.service.CrossroadService; import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; -import org.programmers.signalbuddy.domain.crossroad.service.CrossroadService; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.List; @Validated @RestController @@ -24,4 +28,28 @@ public ResponseEntity saveCrossroadDates(@Min(1) @RequestParam("page") int crossroadService.saveCrossroadDates(page, pageSize); return ResponseEntity.ok().build(); } + + + @GetMapping("/marker") // 저장된 DB 데이터를 기반으로 map에 찍을 marker의 데이터를 point로 가져오기 + public ResponseEntity> pointToMarker(){ + List markers = crossroadService.getAllMarkers(); + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(markers); + } + + + @GetMapping("/state/{id}") // id를 기반으로 신호등 데이터 상태 검색 + public ResponseEntity> markerToState(@PathVariable("id") Long id) { + HttpHeaders headers = new HttpHeaders(); + headers.setCacheControl(CacheControl.noCache().getHeaderValue()); + + List stateRes = crossroadService.checkSignalState(id); + + return ResponseEntity.ok() + .headers(headers) + .contentType(MediaType.APPLICATION_JSON) + .body(stateRes); + } + } diff --git a/src/main/java/org/programmers/signalbuddy/domain/crossroad/controller/WebController.java b/src/main/java/org/programmers/signalbuddy/domain/crossroad/controller/WebController.java new file mode 100644 index 00000000..95b3a971 --- /dev/null +++ b/src/main/java/org/programmers/signalbuddy/domain/crossroad/controller/WebController.java @@ -0,0 +1,17 @@ +package org.programmers.signalbuddy.domain.crossroad.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +@Controller +@RequestMapping("/") +public class WebController { + + @GetMapping + public ModelAndView index(ModelAndView mv) { + mv.setViewName("main"); + return mv; + } +} diff --git a/src/main/java/org/programmers/signalbuddy/domain/crossroad/dto/CrossroadApiResponse.java b/src/main/java/org/programmers/signalbuddy/domain/crossroad/dto/CrossroadApiResponse.java index 20146e4d..a2eb5401 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/crossroad/dto/CrossroadApiResponse.java +++ b/src/main/java/org/programmers/signalbuddy/domain/crossroad/dto/CrossroadApiResponse.java @@ -1,7 +1,6 @@ package org.programmers.signalbuddy.domain.crossroad.dto; -import static org.programmers.signalbuddy.domain.crossroad.service.PointUtil.toPoint; - +import org.programmers.signalbuddy.domain.crossroad.entity.Crossroad; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AccessLevel; @@ -9,6 +8,7 @@ import lombok.Builder; import lombok.Getter; import org.locationtech.jts.geom.Point; +import org.programmers.signalbuddy.domain.crossroad.service.PointUtil; @Getter @Builder @@ -28,7 +28,14 @@ public class CrossroadApiResponse { @JsonProperty("mapCtptIntLot") private Double lng; // 경도 - public Point getPoint() { - return toPoint(this.lat, this.lng); + public Point toPoint() { + return PointUtil.toPoint(this.lat, this.lng); + } + + public CrossroadApiResponse(Crossroad crossroad) { + this.crossroadApiId = crossroad.getCrossroadApiId(); + this.name = crossroad.getName(); + this.lat = crossroad.getCoordinate().getY(); + this.lng = crossroad.getCoordinate().getX(); } } diff --git a/src/main/java/org/programmers/signalbuddy/domain/crossroad/dto/CrossroadStateApiResponse.java b/src/main/java/org/programmers/signalbuddy/domain/crossroad/dto/CrossroadStateApiResponse.java new file mode 100644 index 00000000..03274dab --- /dev/null +++ b/src/main/java/org/programmers/signalbuddy/domain/crossroad/dto/CrossroadStateApiResponse.java @@ -0,0 +1,70 @@ +package org.programmers.signalbuddy.domain.crossroad.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class CrossroadStateApiResponse { + + @JsonProperty("itstId") + private String crossroadApiId; + + // e,w,s,n,ne,nw,se,sw 8방위 + // p : 사람, rs: remain second (남은 시간), sn: state name (상태 이름) + + @JsonProperty("ntPdsgRmdrCs") + private int nprs; + + @JsonProperty("etPdsgRmdrCs") + private int eprs; + + @JsonProperty("stPdsgRmdrCs") + private int sprs; + + @JsonProperty("wtPdsgRmdrCs") + private int wprs; + + @JsonProperty("nePdsgRmdrCs") + private int neprs; + + @JsonProperty("nwPdsgRmdrCs") + private int nwprs; + + @JsonProperty("swPdsgRmdrCs") + private int swprs; + + @JsonProperty("sePdsgRmdrCs") + private int seprs; + + @JsonProperty("ntPdsgStatNm") + private SignalState npsn; + + @JsonProperty("etPdsgStatNm") + private SignalState epsn; + + @JsonProperty("wtPdsgStatNm") + private SignalState wpsn; + + @JsonProperty("stPdsgStatNm") + private SignalState spsn; + + @JsonProperty("nePdsgStatNm") + private SignalState nepsn; + + @JsonProperty("nwPdsgStatNm") + private SignalState nwpsn; + + @JsonProperty("sePdsgStatNm") + private SignalState sepsn; + + @JsonProperty("swPdsgStatNm") + private SignalState swpsn; + +} diff --git a/src/main/java/org/programmers/signalbuddy/domain/crossroad/dto/SignalState.java b/src/main/java/org/programmers/signalbuddy/domain/crossroad/dto/SignalState.java new file mode 100644 index 00000000..6ba27b65 --- /dev/null +++ b/src/main/java/org/programmers/signalbuddy/domain/crossroad/dto/SignalState.java @@ -0,0 +1,32 @@ +package org.programmers.signalbuddy.domain.crossroad.dto; + + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum SignalState { + GREEN("protected-Movement-Allowed",true), // 신호등이 녹색, 이동 보장 상태 + GRAY("permissive-Movement-Allowed",true), // 신호등이 황생, 이동 가능 상태 + RED("stop-And-Remain",false); // 신호등이 적색, 정지 상태 + + private String state; + private boolean can_cross; + + @JsonValue // 데이터 직렬화 java -> JSON + public String getState() { + return name().toLowerCase(); + } + + @JsonCreator // 데이터 역직렬화 JSON -> java + public static SignalState fromState(String state) { + for(SignalState signalState : SignalState.values()) { + if(signalState.state.equals(state)) { + return signalState; + } + } + throw new IllegalArgumentException("Unknown name: " + state); + } +} diff --git a/src/main/java/org/programmers/signalbuddy/domain/crossroad/entity/Crossroad.java b/src/main/java/org/programmers/signalbuddy/domain/crossroad/entity/Crossroad.java index 9858c7c0..efd4e65c 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/crossroad/entity/Crossroad.java +++ b/src/main/java/org/programmers/signalbuddy/domain/crossroad/entity/Crossroad.java @@ -1,19 +1,10 @@ package org.programmers.signalbuddy.domain.crossroad.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.ToString; -import org.locationtech.jts.geom.Point; import org.programmers.signalbuddy.domain.basetime.BaseTimeEntity; import org.programmers.signalbuddy.domain.crossroad.dto.CrossroadApiResponse; +import jakarta.persistence.*; +import lombok.*; +import org.locationtech.jts.geom.Point; @Entity(name = "crossroads") @Getter @@ -39,6 +30,7 @@ public class Crossroad extends BaseTimeEntity { public Crossroad(CrossroadApiResponse response) { this.crossroadApiId = response.getCrossroadApiId(); this.name = response.getName(); - this.coordinate = response.getPoint(); + this.coordinate = response.toPoint(); } + } \ No newline at end of file diff --git a/src/main/java/org/programmers/signalbuddy/domain/crossroad/exception/CrossroadErrorCode.java b/src/main/java/org/programmers/signalbuddy/domain/crossroad/exception/CrossroadErrorCode.java index 07af81b2..f72bf4bf 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/crossroad/exception/CrossroadErrorCode.java +++ b/src/main/java/org/programmers/signalbuddy/domain/crossroad/exception/CrossroadErrorCode.java @@ -1,9 +1,9 @@ package org.programmers.signalbuddy.domain.crossroad.exception; +import org.programmers.signalbuddy.global.exception.ErrorCode; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; -import org.programmers.signalbuddy.global.exception.ErrorCode; import org.springframework.http.HttpStatus; @Getter diff --git a/src/main/java/org/programmers/signalbuddy/domain/crossroad/service/CrossroadProvider.java b/src/main/java/org/programmers/signalbuddy/domain/crossroad/service/CrossroadProvider.java index 0c7663f1..e6b2f88e 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/crossroad/service/CrossroadProvider.java +++ b/src/main/java/org/programmers/signalbuddy/domain/crossroad/service/CrossroadProvider.java @@ -1,16 +1,18 @@ package org.programmers.signalbuddy.domain.crossroad.service; -import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.programmers.signalbuddy.domain.crossroad.dto.CrossroadApiResponse; +import org.programmers.signalbuddy.domain.crossroad.dto.CrossroadStateApiResponse; import org.programmers.signalbuddy.domain.crossroad.exception.CrossroadErrorCode; import org.programmers.signalbuddy.global.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; +import java.util.List; + @Slf4j @Component @RequiredArgsConstructor @@ -20,6 +22,8 @@ public class CrossroadProvider { private String API_KEY; @Value("${t-data-api.crossroad-api}") private String CROSSROAD_API_URL; + @Value("${t-data-api.traffic-light-api}") + private String SIGNAL_STATE_URL; private final WebClient webClient; @@ -34,9 +38,27 @@ public List requestCrossroadApi(int page, int pageSize) { .retrieve() .bodyToMono(new ParameterizedTypeReference>() {}) .onErrorMap(e -> { - log.error("{}\n{}", e.getMessage(), e.getCause()); + log.error("{}\n", e.getMessage(), e.getCause()); throw new BusinessException(CrossroadErrorCode.CROSSROAD_API_REQUEST_FAILED); }) .block(); } + + public List requestCrossroadStateApi(Long id) { + return webClient.get() + .uri(SIGNAL_STATE_URL, + uriBuilder -> uriBuilder + .queryParam("apiKey",API_KEY) + .queryParam("itstId",id) + .queryParam("pageNo",1) + .queryParam("numOfRows",1) + .build()) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}) + .onErrorMap(e->{ + log.error("{}\n", e.getMessage(), e.getCause()); + throw new BusinessException(CrossroadErrorCode.CROSSROAD_API_REQUEST_FAILED); + }) + .block(); + } } diff --git a/src/main/java/org/programmers/signalbuddy/domain/crossroad/service/CrossroadService.java b/src/main/java/org/programmers/signalbuddy/domain/crossroad/service/CrossroadService.java index 99ad07e7..5b7211d4 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/crossroad/service/CrossroadService.java +++ b/src/main/java/org/programmers/signalbuddy/domain/crossroad/service/CrossroadService.java @@ -1,17 +1,19 @@ package org.programmers.signalbuddy.domain.crossroad.service; -import java.util.ArrayList; -import java.util.List; -import lombok.RequiredArgsConstructor; import org.programmers.signalbuddy.domain.crossroad.dto.CrossroadApiResponse; +import org.programmers.signalbuddy.domain.crossroad.dto.CrossroadStateApiResponse; import org.programmers.signalbuddy.domain.crossroad.entity.Crossroad; import org.programmers.signalbuddy.domain.crossroad.exception.CrossroadErrorCode; import org.programmers.signalbuddy.domain.crossroad.repository.CrossroadRepository; import org.programmers.signalbuddy.global.exception.BusinessException; +import lombok.RequiredArgsConstructor; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.List; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -38,4 +40,19 @@ public void saveCrossroadDates(int page, int pageSize) { throw new BusinessException(CrossroadErrorCode.ALREADY_EXIST_CROSSROAD); } } + + public List checkSignalState(Long id) { // id값으로 신호등의 상태를 검색 + return crossroadProvider.requestCrossroadStateApi(id); + } + + public List getAllMarkers(){ + List crossroads = crossroadRepository.findAll(); + List responseList = new ArrayList<>(); + + for(Crossroad crossroad : crossroads){ + responseList.add(new CrossroadApiResponse(crossroad)); + } + + return responseList; + } } diff --git a/src/main/java/org/programmers/signalbuddy/domain/feedback/repository/CustomFeedbackRepositoryImpl.java b/src/main/java/org/programmers/signalbuddy/domain/feedback/repository/CustomFeedbackRepositoryImpl.java index 2efa7490..5fe479f2 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/feedback/repository/CustomFeedbackRepositoryImpl.java +++ b/src/main/java/org/programmers/signalbuddy/domain/feedback/repository/CustomFeedbackRepositoryImpl.java @@ -72,7 +72,8 @@ public Page findPagedByMember(Long memberId, Pageable pageable .offset(pageable.getOffset()).limit(pageable.getPageSize()) .orderBy(new OrderSpecifier<>(Order.DESC, feedback.createdAt)).fetch(); final Long count = jpaQueryFactory.select(feedback.count()).from(feedback).join(member) - .on(member.memberId.eq(memberId)).fetchOne(); + .on(feedback.member.eq(member).and(member.memberId.eq(memberId))) + .fetchOne(); return new PageImpl<>(responses, pageable, count != null ? count : 0); } diff --git a/src/main/java/org/programmers/signalbuddy/domain/feedback/repository/FeedbackRepository.java b/src/main/java/org/programmers/signalbuddy/domain/feedback/repository/FeedbackRepository.java index ccba45c6..f0389f91 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/feedback/repository/FeedbackRepository.java +++ b/src/main/java/org/programmers/signalbuddy/domain/feedback/repository/FeedbackRepository.java @@ -1,6 +1,8 @@ package org.programmers.signalbuddy.domain.feedback.repository; import org.programmers.signalbuddy.domain.feedback.entity.Feedback; +import org.programmers.signalbuddy.domain.feedback.exception.FeedbackErrorCode; +import org.programmers.signalbuddy.global.exception.BusinessException; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -8,4 +10,8 @@ public interface FeedbackRepository extends JpaRepository, CustomFeedbackRepository { + default Feedback findByIdOrThrow(Long id) { + return findById(id) + .orElseThrow(() -> new BusinessException(FeedbackErrorCode.NOT_FOUND_FEEDBACK)); + } } diff --git a/src/main/java/org/programmers/signalbuddy/domain/like/controller/LikeController.java b/src/main/java/org/programmers/signalbuddy/domain/like/controller/LikeController.java index 5e254971..2a6e2f38 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/like/controller/LikeController.java +++ b/src/main/java/org/programmers/signalbuddy/domain/like/controller/LikeController.java @@ -18,14 +18,14 @@ @Tag(name = "좋아요 API") @RestController -@RequestMapping("/api/likes") +@RequestMapping("/api/feedbacks") @RequiredArgsConstructor public class LikeController { private final LikeService likeService; @Operation(summary = "좋아요 추가") - @PostMapping("/{feedbackId}") + @PostMapping("/{feedbackId}/like") public ResponseEntity addLike(@PathVariable("feedbackId") Long feedbackId, @CurrentUser CustomUser2Member user) { likeService.addLike(feedbackId, user); @@ -33,14 +33,14 @@ public ResponseEntity addLike(@PathVariable("feedbackId") Long feedbackId, } @Operation(summary = "좋아요 유무 확인") - @GetMapping("/exist/{feedbackId}") + @GetMapping("/{feedbackId}/exist") public ResponseEntity existsLike(@PathVariable("feedbackId") Long feedbackId, @CurrentUser CustomUser2Member user) { return ResponseEntity.ok(likeService.existsLike(feedbackId, user)); } @Operation(summary = "좋아요 취소") - @DeleteMapping("/{feedbackId}") + @DeleteMapping("/{feedbackId}/like") public ResponseEntity deleteLike(@PathVariable("feedbackId") Long feedbackId, @CurrentUser CustomUser2Member user) { likeService.deleteLike(feedbackId, user); diff --git a/src/main/java/org/programmers/signalbuddy/domain/like/repository/LikeRepository.java b/src/main/java/org/programmers/signalbuddy/domain/like/repository/LikeRepository.java index 0fa74564..9a7eff8b 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/like/repository/LikeRepository.java +++ b/src/main/java/org/programmers/signalbuddy/domain/like/repository/LikeRepository.java @@ -4,6 +4,8 @@ import org.programmers.signalbuddy.domain.like.entity.Like; import org.programmers.signalbuddy.domain.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -11,5 +13,9 @@ public interface LikeRepository extends JpaRepository { void deleteByMemberAndFeedback(Member member, Feedback feedback); - boolean existsByMemberAndFeedback(Member member, Feedback feedback); + @Query("SELECT CASE WHEN count(*)> 0 THEN true ELSE false END " + + "FROM likes l " + + "WHERE l.member.memberId = :memberId AND l.feedback.feedbackId = :feedbackId") + boolean existsByMemberAndFeedback(@Param("memberId") Long memberId, + @Param("feedbackId") Long feedbackId); } diff --git a/src/main/java/org/programmers/signalbuddy/domain/like/service/LikeService.java b/src/main/java/org/programmers/signalbuddy/domain/like/service/LikeService.java index 3e134e9f..b8582cd5 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/like/service/LikeService.java +++ b/src/main/java/org/programmers/signalbuddy/domain/like/service/LikeService.java @@ -2,16 +2,13 @@ import lombok.RequiredArgsConstructor; import org.programmers.signalbuddy.domain.feedback.entity.Feedback; -import org.programmers.signalbuddy.domain.feedback.exception.FeedbackErrorCode; import org.programmers.signalbuddy.domain.feedback.repository.FeedbackRepository; import org.programmers.signalbuddy.domain.like.dto.LikeExistResponse; import org.programmers.signalbuddy.domain.like.entity.Like; import org.programmers.signalbuddy.domain.like.repository.LikeRepository; import org.programmers.signalbuddy.domain.member.entity.Member; -import org.programmers.signalbuddy.domain.member.exception.MemberErrorCode; import org.programmers.signalbuddy.domain.member.repository.MemberRepository; import org.programmers.signalbuddy.global.dto.CustomUser2Member; -import org.programmers.signalbuddy.global.exception.BusinessException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,31 +23,22 @@ public class LikeService { @Transactional public void addLike(Long feedbackId, CustomUser2Member user) { - Feedback feedback = feedbackRepository.findById(feedbackId) - .orElseThrow(() -> new BusinessException(FeedbackErrorCode.NOT_FOUND_FEEDBACK)); - Member member = memberRepository.findById(user.getMemberId()) - .orElseThrow(() -> new BusinessException(MemberErrorCode.NOT_FOUND_MEMBER)); + Feedback feedback = feedbackRepository.findByIdOrThrow(feedbackId); + Member member = memberRepository.findByIdOrThrow(user.getMemberId()); likeRepository.save(Like.create(member, feedback)); feedback.increaseLike(); } public LikeExistResponse existsLike(Long feedbackId, CustomUser2Member user) { - Feedback feedback = feedbackRepository.findById(feedbackId) - .orElseThrow(() -> new BusinessException(FeedbackErrorCode.NOT_FOUND_FEEDBACK)); - Member member = memberRepository.findById(user.getMemberId()) - .orElseThrow(() -> new BusinessException(MemberErrorCode.NOT_FOUND_MEMBER)); - - boolean isExisted = likeRepository.existsByMemberAndFeedback(member, feedback); + boolean isExisted = likeRepository.existsByMemberAndFeedback(user.getMemberId(), feedbackId); return new LikeExistResponse(isExisted); } @Transactional public void deleteLike(Long feedbackId, CustomUser2Member user) { - Feedback feedback = feedbackRepository.findById(feedbackId) - .orElseThrow(() -> new BusinessException(FeedbackErrorCode.NOT_FOUND_FEEDBACK)); - Member member = memberRepository.findById(user.getMemberId()) - .orElseThrow(() -> new BusinessException(MemberErrorCode.NOT_FOUND_MEMBER)); + Feedback feedback = feedbackRepository.findByIdOrThrow(feedbackId); + Member member = memberRepository.findByIdOrThrow(user.getMemberId()); likeRepository.deleteByMemberAndFeedback(member, feedback); feedback.decreaseLike(); diff --git a/src/main/java/org/programmers/signalbuddy/domain/member/controller/MemberWebController.java b/src/main/java/org/programmers/signalbuddy/domain/member/controller/MemberWebController.java index 8b513127..7762901b 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/member/controller/MemberWebController.java +++ b/src/main/java/org/programmers/signalbuddy/domain/member/controller/MemberWebController.java @@ -1,43 +1,51 @@ package org.programmers.signalbuddy.domain.member.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.programmers.signalbuddy.domain.bookmark.dto.BookmarkResponse; import org.programmers.signalbuddy.domain.bookmark.service.BookmarkService; import org.programmers.signalbuddy.domain.feedback.dto.FeedbackResponse; import org.programmers.signalbuddy.domain.feedback.service.FeedbackService; -import org.programmers.signalbuddy.domain.member.dto.MemberResponse; +import org.programmers.signalbuddy.domain.member.dto.MemberJoinRequest; import org.programmers.signalbuddy.domain.member.service.MemberService; +import org.programmers.signalbuddy.global.annotation.CurrentUser; +import org.programmers.signalbuddy.global.dto.CustomUser2Member; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; +@Slf4j @Controller @RequestMapping("members") @RequiredArgsConstructor public class MemberWebController { - private final MemberService memberService; private final BookmarkService bookmarkService; private final FeedbackService feedbackService; + private final MemberService memberService; + + @ModelAttribute("user") + public CustomUser2Member currentUser(@CurrentUser CustomUser2Member user) { + return user; + } - @GetMapping("{id}") - public ModelAndView getMemberView(ModelAndView mv, @PathVariable Long id) { + @GetMapping + public ModelAndView getMemberView(ModelAndView mv) { mv.setViewName("member/info"); - final MemberResponse member = memberService.getMember(id); - mv.addObject("member", member); return mv; } - @GetMapping("{id}/edit") - public ModelAndView editMemberView(ModelAndView mv, @PathVariable Long id) { + @GetMapping("edit") + public ModelAndView editMemberView(ModelAndView mv) { mv.setViewName("member/edit"); - final MemberResponse member = memberService.getMember(id); - mv.addObject("member", member); return mv; } @@ -66,5 +74,22 @@ public ModelAndView findPagedFeedbacks(@PageableDefault(page = 0, size = 5) Page mv.setViewName("member/feedback/list"); return mv; } + + @GetMapping("/signup") + public ModelAndView signup(ModelAndView mv) { + + mv.addObject("memberJoinRequest", new MemberJoinRequest()); + mv.setViewName("member/signup"); + return mv; + } + + @PostMapping("/signup") + public ModelAndView registerMember(@ModelAttribute @Valid MemberJoinRequest joinMember, + ModelAndView mv) { + + memberService.joinMember(joinMember); + mv.setViewName("redirect:/members/login"); + return mv; + } } diff --git a/src/main/java/org/programmers/signalbuddy/domain/member/dto/MemberJoinRequest.java b/src/main/java/org/programmers/signalbuddy/domain/member/dto/MemberJoinRequest.java index 92f64112..00599562 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/member/dto/MemberJoinRequest.java +++ b/src/main/java/org/programmers/signalbuddy/domain/member/dto/MemberJoinRequest.java @@ -9,13 +9,19 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; +@Setter @Builder @Getter -@NoArgsConstructor(access = AccessLevel.PRIVATE) -@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor +@AllArgsConstructor public class MemberJoinRequest { + @Schema(description = "프로필 사진", requiredMode = RequiredMode.NOT_REQUIRED, defaultValue = "/images/member/profile-icon.png") + private MultipartFile profileImageUrl; + @Email(message = "이메일 형식에 맞지 않습니다.") @NotBlank(message = "이메일은 필수 입력 사항입니다.") @Schema(description = "이메일", requiredMode = RequiredMode.NOT_REQUIRED, defaultValue = "udpate@example.com") @@ -28,7 +34,4 @@ public class MemberJoinRequest { @NotBlank(message = "비밀번호는 필수 입력 사항입니다.") @Schema(description = "비밀번호", requiredMode = RequiredMode.NOT_REQUIRED, defaultValue = "password123") private String password; - - @Schema(description = "프로필 사진", requiredMode = RequiredMode.NOT_REQUIRED, defaultValue = "/images/test.png") - private String profileImageUrl; } diff --git a/src/main/java/org/programmers/signalbuddy/domain/member/dto/MemberUpdateRequest.java b/src/main/java/org/programmers/signalbuddy/domain/member/dto/MemberUpdateRequest.java index 5919e453..de90783b 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/member/dto/MemberUpdateRequest.java +++ b/src/main/java/org/programmers/signalbuddy/domain/member/dto/MemberUpdateRequest.java @@ -27,6 +27,6 @@ public class MemberUpdateRequest { @Schema(description = "닉네임", requiredMode = RequiredMode.NOT_REQUIRED, defaultValue = "Nickname") private String nickname; - @Schema(description = "프로필 사진", requiredMode = RequiredMode.NOT_REQUIRED, defaultValue = "/images/test.png") + @Schema(description = "프로필 사진", requiredMode = RequiredMode.NOT_REQUIRED, defaultValue = "test.png") private String profileImageUrl; } diff --git a/src/main/java/org/programmers/signalbuddy/domain/member/entity/Member.java b/src/main/java/org/programmers/signalbuddy/domain/member/entity/Member.java index 74affa7e..eea0443f 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/member/entity/Member.java +++ b/src/main/java/org/programmers/signalbuddy/domain/member/entity/Member.java @@ -18,6 +18,7 @@ import org.programmers.signalbuddy.domain.member.entity.enums.MemberRole; import org.programmers.signalbuddy.domain.member.entity.enums.MemberStatus; import org.programmers.signalbuddy.global.dto.CustomUser2Member; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @Entity(name = "members") @Getter @@ -49,12 +50,22 @@ public class Member extends BaseTimeEntity { @Enumerated(EnumType.STRING) private MemberStatus memberStatus; - public void updateMember(MemberUpdateRequest request) { + // 관리자인지 확인 + public static boolean isAdmin(Member member) { + return MemberRole.ADMIN.equals(member.getRole()); + } + + // 요청자와 작성자가 다른 경우 + public static boolean isNotSameMember(CustomUser2Member user, Member member) { + return !user.getMemberId().equals(member.getMemberId()); + } + + public void updateMember(MemberUpdateRequest request, String encodedPassword) { if (request.getEmail() != null) { this.email = request.getEmail(); } if (request.getPassword() != null) { - this.password = request.getPassword(); // TODO: 암호화 적용 + this.password = encodedPassword; } if (request.getNickname() != null) { this.nickname = request.getNickname(); @@ -67,18 +78,4 @@ public void updateMember(MemberUpdateRequest request) { public void softDelete() { this.memberStatus = MemberStatus.WITHDRAWAL; } - - // 관리자인지 확인 - public static boolean isAdmin(Member member) { - return MemberRole.ADMIN.equals(member.getRole()); - } - - // 요청자와 작성자가 다른 경우 - public static boolean isNotSameMember(CustomUser2Member user, Member member) { - return !user.getMemberId().equals(member.getMemberId()); - } - - public void setPassword(String password) { - this.password = password; - } } diff --git a/src/main/java/org/programmers/signalbuddy/domain/member/exception/MemberErrorCode.java b/src/main/java/org/programmers/signalbuddy/domain/member/exception/MemberErrorCode.java index 3d788793..5e572c32 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/member/exception/MemberErrorCode.java +++ b/src/main/java/org/programmers/signalbuddy/domain/member/exception/MemberErrorCode.java @@ -12,7 +12,9 @@ public enum MemberErrorCode implements ErrorCode { NOT_FOUND_MEMBER(HttpStatus.NOT_FOUND, 60000, "해당 사용자를 찾을 수 없습니다."), ALREADY_EXIST_EMAIL(HttpStatus.CONFLICT, 60001, "이미 존재하는 이메일 입니다."), - PROFILE_IMAGE_LOAD_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 60002, "프로필 이미지 로드 중 오류가 발생했습니다."); + PROFILE_IMAGE_LOAD_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 60002, "프로필 이미지 로드 중 오류가 발생했습니다."), + WITHDRAWN_MEMBER(HttpStatus.FORBIDDEN, 60003, "탈퇴한 회원입니다."), + PROFILE_IMAGE_UPLOAD_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, 60004, "프로필 이미지 저장 중 오류가 발생했습니다."); private HttpStatus httpStatus; private int code; diff --git a/src/main/java/org/programmers/signalbuddy/domain/member/repository/CustomMemberRepository.java b/src/main/java/org/programmers/signalbuddy/domain/member/repository/CustomMemberRepository.java index 97d636ce..a2ec6e5c 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/member/repository/CustomMemberRepository.java +++ b/src/main/java/org/programmers/signalbuddy/domain/member/repository/CustomMemberRepository.java @@ -1,9 +1,11 @@ package org.programmers.signalbuddy.domain.member.repository; import org.programmers.signalbuddy.domain.admin.dto.WithdrawalMemberResponse; +import org.programmers.signalbuddy.domain.member.entity.Member; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; public interface CustomMemberRepository { + Page findAllMembers(Pageable pageable); Page findAllWithdrawMembers(Pageable pageable); } diff --git a/src/main/java/org/programmers/signalbuddy/domain/member/repository/CustomMemberRepositoryImpl.java b/src/main/java/org/programmers/signalbuddy/domain/member/repository/CustomMemberRepositoryImpl.java index b10250f4..91fd4104 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/member/repository/CustomMemberRepositoryImpl.java +++ b/src/main/java/org/programmers/signalbuddy/domain/member/repository/CustomMemberRepositoryImpl.java @@ -8,6 +8,9 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.programmers.signalbuddy.domain.admin.dto.WithdrawalMemberResponse; +import org.programmers.signalbuddy.domain.member.entity.Member; +import org.programmers.signalbuddy.domain.member.entity.QMember; +import org.programmers.signalbuddy.domain.member.entity.enums.MemberRole; import org.programmers.signalbuddy.domain.member.entity.enums.MemberStatus; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -22,8 +25,28 @@ public class CustomMemberRepositoryImpl implements CustomMemberRepository { WithdrawalMemberResponse.class, member.memberId, member.email, member.nickname, member.profileImageUrl, member.role, member.memberStatus, member.createdAt, member.updatedAt); + private static final QMember qmember= QMember.member; + private final JPAQueryFactory jpaQueryFactory; + @Override + public Page findAllMembers(Pageable pageable) { + List members = jpaQueryFactory + .select(qmember) + .from(member) + .where(member.role.eq(MemberRole.USER)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + long total = jpaQueryFactory + .select(qmember) + .from(member) + .where(member.role.eq(MemberRole.USER)) + .fetchCount(); + + return new PageImpl<>(members, pageable, total); + } @Override public Page findAllWithdrawMembers(Pageable pageable) { diff --git a/src/main/java/org/programmers/signalbuddy/domain/member/repository/MemberRepository.java b/src/main/java/org/programmers/signalbuddy/domain/member/repository/MemberRepository.java index 0d9b22f4..8f602753 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/member/repository/MemberRepository.java +++ b/src/main/java/org/programmers/signalbuddy/domain/member/repository/MemberRepository.java @@ -2,6 +2,8 @@ import java.util.Optional; import org.programmers.signalbuddy.domain.member.entity.Member; +import org.programmers.signalbuddy.domain.member.exception.MemberErrorCode; +import org.programmers.signalbuddy.global.exception.BusinessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -19,4 +21,9 @@ public interface MemberRepository extends JpaRepository, CustomMem Member save(Member member); void delete(Member member); + + default Member findByIdOrThrow(Long id) { + return findById(id) + .orElseThrow(() -> new BusinessException(MemberErrorCode.NOT_FOUND_MEMBER)); + } } diff --git a/src/main/java/org/programmers/signalbuddy/domain/member/service/MemberService.java b/src/main/java/org/programmers/signalbuddy/domain/member/service/MemberService.java index 46b03bd7..2f802198 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/member/service/MemberService.java +++ b/src/main/java/org/programmers/signalbuddy/domain/member/service/MemberService.java @@ -1,9 +1,12 @@ package org.programmers.signalbuddy.domain.member.service; +import java.io.IOException; import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.programmers.signalbuddy.domain.member.dto.MemberJoinRequest; @@ -16,21 +19,23 @@ import org.programmers.signalbuddy.domain.member.mapper.MemberMapper; import org.programmers.signalbuddy.domain.member.repository.MemberRepository; import org.programmers.signalbuddy.global.exception.BusinessException; +import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; @Slf4j @Service @RequiredArgsConstructor public class MemberService { - private final String FILE_PATH = "images/"; // TODO : 프로퍼티로 이동 - private final MemberRepository memberRepository; + @Value("${file-path}") + private String filePath; private BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); public MemberResponse getMember(Long id) { @@ -44,14 +49,15 @@ public MemberResponse getMember(Long id) { public MemberResponse updateMember(Long id, MemberUpdateRequest memberUpdateRequest) { final Member member = memberRepository.findById(id) .orElseThrow(() -> new BusinessException(MemberErrorCode.NOT_FOUND_MEMBER)); - member.updateMember(memberUpdateRequest); + final String encodedPassword = bCryptPasswordEncoder.encode( + memberUpdateRequest.getPassword()); + member.updateMember(memberUpdateRequest, encodedPassword); log.info("Member updated: {}", member); return MemberMapper.INSTANCE.toDto(member); } @Transactional public MemberResponse deleteMember(Long id) { - // TODO : id 검증 로직 추가 final Member member = memberRepository.findById(id) .orElseThrow(() -> new BusinessException(MemberErrorCode.NOT_FOUND_MEMBER)); member.softDelete(); @@ -67,10 +73,16 @@ public MemberResponse joinMember(MemberJoinRequest memberJoinRequest) { throw new BusinessException(MemberErrorCode.ALREADY_EXIST_EMAIL); } + String profilePath = "none"; + + if(!memberJoinRequest.getProfileImageUrl().isEmpty()) { + profilePath = saveProfileImage(memberJoinRequest.getProfileImageUrl()); + } + Member joinMember = Member.builder().email(memberJoinRequest.getEmail()) .nickname(memberJoinRequest.getNickname()) .password(bCryptPasswordEncoder.encode(memberJoinRequest.getPassword())) - .profileImageUrl(memberJoinRequest.getProfileImageUrl()) + .profileImageUrl(profilePath) .memberStatus(MemberStatus.ACTIVITY).role(MemberRole.USER).build(); memberRepository.save(joinMember); @@ -79,7 +91,8 @@ public MemberResponse joinMember(MemberJoinRequest memberJoinRequest) { public Resource getProfileImage(String filename) { try { - final Path path = Paths.get(FILE_PATH).resolve(filename); + Path directoryPath = Paths.get("src", "main", "resources","static", "images"); + final Path path = Paths.get(directoryPath.toString()).resolve(filename); if (Files.notExists(path)) { return new ClassPathResource("static/images/member/profile-icon.png"); // 프로필 이미지가 없을 경우 기본 이미지 @@ -89,4 +102,25 @@ public Resource getProfileImage(String filename) { throw new BusinessException(MemberErrorCode.PROFILE_IMAGE_LOAD_ERROR); } } + + public String saveProfileImage(MultipartFile profileImage) { + + // static/images 경로 설정 + Path directoryPath = Paths.get("src", "main", "resources","static", "images"); + + String originalFilename = profileImage.getOriginalFilename(); + String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); + + String newFilename = UUID.randomUUID().toString() + extension; + Path savePath = directoryPath.resolve(newFilename); + + try { + Files.copy(profileImage.getInputStream(), savePath, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + log.error("Error while saving profile image: ", e); + throw new BusinessException(MemberErrorCode.PROFILE_IMAGE_UPLOAD_FAILURE); + } + + return newFilename.toString(); + } } diff --git a/src/main/java/org/programmers/signalbuddy/domain/social/repository/SocialProviderRepository.java b/src/main/java/org/programmers/signalbuddy/domain/social/repository/SocialProviderRepository.java index 30e231fd..d5e6b111 100644 --- a/src/main/java/org/programmers/signalbuddy/domain/social/repository/SocialProviderRepository.java +++ b/src/main/java/org/programmers/signalbuddy/domain/social/repository/SocialProviderRepository.java @@ -7,4 +7,5 @@ @Repository public interface SocialProviderRepository extends JpaRepository { + boolean existsByOauthProviderAndSocialId(String provider, String providerId); } diff --git a/src/main/java/org/programmers/signalbuddy/global/config/SecurityConfig.java b/src/main/java/org/programmers/signalbuddy/global/config/SecurityConfig.java index a2213b84..943edcaa 100644 --- a/src/main/java/org/programmers/signalbuddy/global/config/SecurityConfig.java +++ b/src/main/java/org/programmers/signalbuddy/global/config/SecurityConfig.java @@ -1,7 +1,10 @@ package org.programmers.signalbuddy.global.config; +import lombok.RequiredArgsConstructor; import org.programmers.signalbuddy.global.security.filter.UserAuthenticationFilter; import org.programmers.signalbuddy.global.security.handler.CustomAuthenticationSuccessHandler; + +import org.programmers.signalbuddy.global.security.oauth.CustomOAuth2UserService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -13,6 +16,7 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +@RequiredArgsConstructor @Configuration @EnableWebSecurity public class SecurityConfig { @@ -27,6 +31,8 @@ CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler() { return new CustomAuthenticationSuccessHandler(); } + private final CustomOAuth2UserService customOAuth2UserService; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -42,14 +48,16 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/js/**", "/images/**", "/webjars/**").permitAll() - // 로그인 - .requestMatchers("/members/login", "admins/login", "/api/members/join", "/api/admins/join").permitAll() + // 로그인, 회원가입 + .requestMatchers("/members/login", "admins/login", "/api/members/join", + "/api/admins/join", "/members/signup").permitAll() // 북마크 .requestMatchers("/api/bookmarks/**", "/bookmarks/**").hasRole("USER") // 댓글 .requestMatchers(HttpMethod.GET, "/api/comments").permitAll() // 교차로 .requestMatchers("/api/crossroads/save").hasRole("ADMIN") + .requestMatchers(HttpMethod.GET,"/api/crossroads/**").permitAll() // 피드백 .requestMatchers(HttpMethod.GET, "/api/feedbacks", "/feedbacks/**").permitAll() // 회원 @@ -58,22 +66,29 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .anyRequest().authenticated() ); - // 로그인 관련 설정 + // 기본 로그인 관련 설정 http .formLogin((auth) -> auth .loginPage("/members/login") .loginProcessingUrl("/login") - // 메인으로 이동하도록 설정 - //.defaultSuccessUrl("/home", true) .successHandler(customAuthenticationSuccessHandler()) .permitAll() ); + // 소셜 로그인 관련 설정 + http + .oauth2Login((oauth) -> oauth + .loginPage("/login") + .userInfoEndpoint(userInfoEndpointConfig -> + userInfoEndpointConfig.userService(customOAuth2UserService)) + .successHandler(customAuthenticationSuccessHandler()) + .permitAll()); + // 로그아웃 관련 설정 http .logout((auth) -> auth .logoutUrl("/logout") - .logoutSuccessUrl("/login") + .logoutSuccessUrl("/members/login") .deleteCookies("JSESSIONID") .invalidateHttpSession(true) .clearAuthentication(true)); diff --git a/src/main/java/org/programmers/signalbuddy/global/dto/CustomUser2Member.java b/src/main/java/org/programmers/signalbuddy/global/dto/CustomUser2Member.java index abe02507..cbca2a1a 100644 --- a/src/main/java/org/programmers/signalbuddy/global/dto/CustomUser2Member.java +++ b/src/main/java/org/programmers/signalbuddy/global/dto/CustomUser2Member.java @@ -3,7 +3,8 @@ import lombok.Getter; import org.programmers.signalbuddy.domain.member.entity.enums.MemberRole; import org.programmers.signalbuddy.domain.member.entity.enums.MemberStatus; -import org.programmers.signalbuddy.global.security.CustomUserDetails; +import org.programmers.signalbuddy.global.security.basic.CustomUserDetails; +import org.programmers.signalbuddy.global.security.oauth.CustomOAuth2User; @Getter public class CustomUser2Member { @@ -24,5 +25,14 @@ public CustomUser2Member(CustomUserDetails customUserDetails) { this.status = customUserDetails.getStatus(); } + public CustomUser2Member(CustomOAuth2User customOAuth2User) { + this.memberId = customOAuth2User.getMemberId(); + this.email = customOAuth2User.getEmail(); + this.profileImageUrl = customOAuth2User.getProfileImageUrl(); + this.nickname = customOAuth2User.getNickname(); + this.role = customOAuth2User.getRole(); + this.status = customOAuth2User.getStatus(); + } + public CustomUser2Member(String arg) {} } diff --git a/src/main/java/org/programmers/signalbuddy/global/exception/GlobalErrorCode.java b/src/main/java/org/programmers/signalbuddy/global/exception/GlobalErrorCode.java index b358938a..edaadffb 100644 --- a/src/main/java/org/programmers/signalbuddy/global/exception/GlobalErrorCode.java +++ b/src/main/java/org/programmers/signalbuddy/global/exception/GlobalErrorCode.java @@ -9,8 +9,8 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public enum GlobalErrorCode implements ErrorCode { - BAD_REQUEST(HttpStatus.BAD_REQUEST, 80000, "잘못된 요청입니다."), - SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 90000, "알 수 없는 에러가 발생했습니다. 관리자에게 문의하세요."); + BAD_REQUEST(HttpStatus.BAD_REQUEST, 90000, "잘못된 요청입니다."), + SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 90001, "알 수 없는 에러가 발생했습니다. 관리자에게 문의하세요."); private HttpStatus httpStatus; private int code; diff --git a/src/main/java/org/programmers/signalbuddy/global/security/CustomUserDetails.java b/src/main/java/org/programmers/signalbuddy/global/security/basic/CustomUserDetails.java similarity index 96% rename from src/main/java/org/programmers/signalbuddy/global/security/CustomUserDetails.java rename to src/main/java/org/programmers/signalbuddy/global/security/basic/CustomUserDetails.java index 002e7e37..21bd895e 100644 --- a/src/main/java/org/programmers/signalbuddy/global/security/CustomUserDetails.java +++ b/src/main/java/org/programmers/signalbuddy/global/security/basic/CustomUserDetails.java @@ -1,4 +1,4 @@ -package org.programmers.signalbuddy.global.security; +package org.programmers.signalbuddy.global.security.basic; import java.util.ArrayList; import java.util.Collection; diff --git a/src/main/java/org/programmers/signalbuddy/global/security/CustomUserDetailsService.java b/src/main/java/org/programmers/signalbuddy/global/security/basic/CustomUserDetailsService.java similarity index 79% rename from src/main/java/org/programmers/signalbuddy/global/security/CustomUserDetailsService.java rename to src/main/java/org/programmers/signalbuddy/global/security/basic/CustomUserDetailsService.java index 169e793a..8b070704 100644 --- a/src/main/java/org/programmers/signalbuddy/global/security/CustomUserDetailsService.java +++ b/src/main/java/org/programmers/signalbuddy/global/security/basic/CustomUserDetailsService.java @@ -1,8 +1,9 @@ -package org.programmers.signalbuddy.global.security; +package org.programmers.signalbuddy.global.security.basic; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.programmers.signalbuddy.domain.member.entity.Member; +import org.programmers.signalbuddy.domain.member.entity.enums.MemberStatus; import org.programmers.signalbuddy.domain.member.exception.MemberErrorCode; import org.programmers.signalbuddy.domain.member.repository.MemberRepository; import org.programmers.signalbuddy.global.exception.BusinessException; @@ -26,6 +27,11 @@ public CustomUserDetails loadUserByUsername(String email) throws UsernameNotFoun return new BusinessException(MemberErrorCode.NOT_FOUND_MEMBER); }); + final MemberStatus memberStatus = findMember.getMemberStatus(); + if (memberStatus == MemberStatus.WITHDRAWAL) { + throw new BusinessException(MemberErrorCode.WITHDRAWN_MEMBER); + } + return new CustomUserDetails(findMember.getMemberId(), findMember.getEmail(), findMember.getPassword(), findMember.getProfileImageUrl(), findMember.getNickname(), findMember.getRole(), findMember.getMemberStatus()); diff --git a/src/main/java/org/programmers/signalbuddy/global/security/filter/UserAuthenticationFilter.java b/src/main/java/org/programmers/signalbuddy/global/security/filter/UserAuthenticationFilter.java index eac3388f..ad2fbef8 100644 --- a/src/main/java/org/programmers/signalbuddy/global/security/filter/UserAuthenticationFilter.java +++ b/src/main/java/org/programmers/signalbuddy/global/security/filter/UserAuthenticationFilter.java @@ -5,12 +5,12 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.Collections; -import org.programmers.signalbuddy.global.security.CustomUserDetails; +import java.util.Collection; +import org.programmers.signalbuddy.global.security.basic.CustomUserDetails; +import org.programmers.signalbuddy.global.security.oauth.CustomOAuth2User; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; @@ -20,15 +20,24 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - CustomUserDetails userDetails = (CustomUserDetails) request.getSession() - .getAttribute("user"); - if (userDetails != null) { + Object user = request.getSession().getAttribute("user"); - Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, - null, - userDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(authentication); + if (user instanceof CustomUserDetails) { + CustomUserDetails customUserDetails = (CustomUserDetails) user; + setAuthentication(customUserDetails, customUserDetails.getAuthorities()); + + } else if (user instanceof CustomOAuth2User) { + CustomOAuth2User customOAuth2User = (CustomOAuth2User) user; + setAuthentication(customOAuth2User, customOAuth2User.getAuthorities()); } + filterChain.doFilter(request, response); } + + private void setAuthentication(Object principal, + Collection authorities) { + Authentication authentication = new UsernamePasswordAuthenticationToken(principal, null, + authorities); + SecurityContextHolder.getContext().setAuthentication(authentication); + } } diff --git a/src/main/java/org/programmers/signalbuddy/global/security/handler/CustomAuthenticationSuccessHandler.java b/src/main/java/org/programmers/signalbuddy/global/security/handler/CustomAuthenticationSuccessHandler.java index dfa5ece0..df6ef896 100644 --- a/src/main/java/org/programmers/signalbuddy/global/security/handler/CustomAuthenticationSuccessHandler.java +++ b/src/main/java/org/programmers/signalbuddy/global/security/handler/CustomAuthenticationSuccessHandler.java @@ -4,7 +4,8 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import org.programmers.signalbuddy.global.security.CustomUserDetails; +import org.programmers.signalbuddy.global.security.basic.CustomUserDetails; +import org.programmers.signalbuddy.global.security.oauth.CustomOAuth2User; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; @@ -18,14 +19,17 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo // 임시 경로 String redirectUrl = "/feedbacks"; - if(principal instanceof CustomUserDetails) { + if (principal instanceof CustomUserDetails) { CustomUserDetails customUserDetails = (CustomUserDetails) principal; - if(customUserDetails.getRole().name().contains("ADMIN")){ + if (customUserDetails.getRole().name().contains("ADMIN")) { // 임시 경로 redirectUrl = "/admins/members/list"; } request.getSession().setAttribute("user", customUserDetails); + } else if (principal instanceof CustomOAuth2User) { + CustomOAuth2User customOAuth2User = (CustomOAuth2User) principal; + request.getSession().setAttribute("user", customOAuth2User); } request.getSession().setMaxInactiveInterval(3600); diff --git a/src/main/java/org/programmers/signalbuddy/global/security/oauth/CustomOAuth2User.java b/src/main/java/org/programmers/signalbuddy/global/security/oauth/CustomOAuth2User.java new file mode 100644 index 00000000..d19f6bf6 --- /dev/null +++ b/src/main/java/org/programmers/signalbuddy/global/security/oauth/CustomOAuth2User.java @@ -0,0 +1,50 @@ +package org.programmers.signalbuddy.global.security.oauth; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.programmers.signalbuddy.domain.member.entity.enums.MemberRole; +import org.programmers.signalbuddy.domain.member.entity.enums.MemberStatus; +import org.programmers.signalbuddy.global.security.oauth.response.OAuth2Response; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +@Getter +@AllArgsConstructor +public class CustomOAuth2User implements OAuth2User, Serializable { + + private OAuth2Response oAuth2Response; + private Long memberId; + private String email; + private String password; + private String profileImageUrl; + private String nickname; + private MemberRole role; + private MemberStatus status; + + @Override + public String getName() { + return oAuth2Response.getName(); + } + + @Override + public Map getAttributes() { + return null; + } + + @Override + public Collection getAuthorities() { + Collection authorities = new ArrayList<>(); + authorities.add(new GrantedAuthority() { + @Override + public String getAuthority() { + return "ROLE_" + role.name(); + } + }); + + return authorities; + } +} diff --git a/src/main/java/org/programmers/signalbuddy/global/security/oauth/CustomOAuth2UserService.java b/src/main/java/org/programmers/signalbuddy/global/security/oauth/CustomOAuth2UserService.java new file mode 100644 index 00000000..2d8345c6 --- /dev/null +++ b/src/main/java/org/programmers/signalbuddy/global/security/oauth/CustomOAuth2UserService.java @@ -0,0 +1,82 @@ +package org.programmers.signalbuddy.global.security.oauth; + +import lombok.RequiredArgsConstructor; +import org.programmers.signalbuddy.domain.member.entity.Member; +import org.programmers.signalbuddy.domain.member.entity.enums.MemberRole; +import org.programmers.signalbuddy.domain.member.entity.enums.MemberStatus; +import org.programmers.signalbuddy.domain.member.repository.MemberRepository; +import org.programmers.signalbuddy.domain.social.entity.SocialProvider; +import org.programmers.signalbuddy.domain.social.repository.SocialProviderRepository; +import org.programmers.signalbuddy.global.security.oauth.response.GoogleResponse; +import org.programmers.signalbuddy.global.security.oauth.response.KakaoResponse; +import org.programmers.signalbuddy.global.security.oauth.response.NaverResponse; +import org.programmers.signalbuddy.global.security.oauth.response.OAuth2Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private static final Logger log = LoggerFactory.getLogger(CustomOAuth2UserService.class); + private final MemberRepository memberRepository; + private final SocialProviderRepository socialProviderRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + + OAuth2User oAuth2User = super.loadUser(userRequest); + System.out.println(oAuth2User.getAttributes()); + + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + log.info("registrationId: {}", registrationId); + OAuth2Response oAuth2Response; + + if (registrationId.equals("naver")) { + oAuth2Response = new NaverResponse(oAuth2User.getAttributes()); + } else if (registrationId.equals("google")) { + oAuth2Response = new GoogleResponse(oAuth2User.getAttributes()); + } else if (registrationId.equals("kakao")) { + oAuth2Response = new KakaoResponse(oAuth2User.getAttributes()); + } else { + oAuth2Response = null; + return null; + } + + // 기존 사용자 조회, 없으면 소셜의 이메일과 닉네임을 기반으로 새로운 사용자 생성 + String email = oAuth2Response.getEmail(); + Member saveMember = memberRepository.findByEmail(email) + .orElseGet(() -> { + Member newMember = Member.builder() + .email(email) + .nickname(oAuth2Response.getName()) + .profileImageUrl("static/images/member/profile-icon.png") + .role(MemberRole.USER) + .memberStatus(MemberStatus.ACTIVITY) + .build(); + return memberRepository.save(newMember); + }); + + // 이미 소셜이 저장되어 있는 경우, 중복 저장하지 않음. + if(!socialProviderRepository.existsByOauthProviderAndSocialId(oAuth2Response.getProvider(), oAuth2Response.getProviderId())){ + + SocialProvider socialProvider = SocialProvider.builder() + .oauthProvider(oAuth2Response.getProvider()) + .socialId(oAuth2Response.getProviderId()) + .member(saveMember) + .build(); + + socialProviderRepository.save(socialProvider); + } + + return new CustomOAuth2User(oAuth2Response, saveMember.getMemberId(), email, + saveMember.getPassword(), saveMember.getProfileImageUrl(), saveMember.getNickname(), + saveMember.getRole(), saveMember.getMemberStatus()); + } +} diff --git a/src/main/java/org/programmers/signalbuddy/global/security/oauth/response/GoogleResponse.java b/src/main/java/org/programmers/signalbuddy/global/security/oauth/response/GoogleResponse.java new file mode 100644 index 00000000..b1d6b333 --- /dev/null +++ b/src/main/java/org/programmers/signalbuddy/global/security/oauth/response/GoogleResponse.java @@ -0,0 +1,33 @@ +package org.programmers.signalbuddy.global.security.oauth.response; + +import java.util.Map; + +public class GoogleResponse implements OAuth2Response { + + private final Map attribute; + + public GoogleResponse(Map attribute) { + this.attribute = attribute; + } + + + @Override + public String getProvider() { + return "google"; + } + + @Override + public String getProviderId() { + return attribute.get("sub").toString(); + } + + @Override + public String getEmail() { + return attribute.get("email").toString(); + } + + @Override + public String getName() { + return attribute.get("name").toString(); + } +} diff --git a/src/main/java/org/programmers/signalbuddy/global/security/oauth/response/KakaoResponse.java b/src/main/java/org/programmers/signalbuddy/global/security/oauth/response/KakaoResponse.java new file mode 100644 index 00000000..66981455 --- /dev/null +++ b/src/main/java/org/programmers/signalbuddy/global/security/oauth/response/KakaoResponse.java @@ -0,0 +1,36 @@ +package org.programmers.signalbuddy.global.security.oauth.response; + +import java.util.Map; + +public class KakaoResponse implements OAuth2Response { + + private Map attribute; + private Map kakaoAccountAttributes; + private Map profileAttributes; + + public KakaoResponse(Map attributes) { + this.attribute = attributes; + this.kakaoAccountAttributes = (Map) attribute.get("kakao_account"); + this.profileAttributes = (Map) kakaoAccountAttributes.get("profile"); + } + + @Override + public String getProvider() { + return "kakao"; + } + + @Override + public String getProviderId() { + return attribute.get("id").toString(); + } + + @Override + public String getEmail() { + return kakaoAccountAttributes.get("email").toString(); + } + + @Override + public String getName() { + return profileAttributes.get("nickname").toString(); + } +} diff --git a/src/main/java/org/programmers/signalbuddy/global/security/oauth/response/NaverResponse.java b/src/main/java/org/programmers/signalbuddy/global/security/oauth/response/NaverResponse.java new file mode 100644 index 00000000..4ba889de --- /dev/null +++ b/src/main/java/org/programmers/signalbuddy/global/security/oauth/response/NaverResponse.java @@ -0,0 +1,34 @@ +package org.programmers.signalbuddy.global.security.oauth.response; + + +import java.util.Map; + +public class NaverResponse implements OAuth2Response { + + private final Map attribute; + + public NaverResponse(Map attribute) { + this.attribute = (Map) attribute.get("response"); + } + + + @Override + public String getProvider() { + return "naver"; + } + + @Override + public String getProviderId() { + return attribute.get("id").toString(); + } + + @Override + public String getEmail() { + return attribute.get("email").toString(); + } + + @Override + public String getName() { + return attribute.get("nickname").toString(); + } +} diff --git a/src/main/java/org/programmers/signalbuddy/global/security/oauth/response/OAuth2Response.java b/src/main/java/org/programmers/signalbuddy/global/security/oauth/response/OAuth2Response.java new file mode 100644 index 00000000..7d51adf4 --- /dev/null +++ b/src/main/java/org/programmers/signalbuddy/global/security/oauth/response/OAuth2Response.java @@ -0,0 +1,14 @@ +package org.programmers.signalbuddy.global.security.oauth.response; + +import java.io.Serializable; + +public interface OAuth2Response extends Serializable { + + String getProvider(); + + String getProviderId(); + + String getEmail(); + + String getName(); +} diff --git a/src/main/resources/static/css/admin/detail.css b/src/main/resources/static/css/admin/detail.css new file mode 100644 index 00000000..a9d4a1df --- /dev/null +++ b/src/main/resources/static/css/admin/detail.css @@ -0,0 +1,462 @@ +@import url(https://fonts.googleapis.com/css2?family=Lato&display=swap); + +@import url(https://fonts.googleapis.com/css2?family=Open+Sans&display=swap); + +/*! tailwindcss v3.4.11 | MIT License | https://tailwindcss.com*/ +*, +:after, +:before { + border: 0 solid #e5e7eb; + box-sizing: border-box; +} +:after, +:before { + --tw-content: ""; +} +:host, +html { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + font-family: + Open Sans, + ui-sans-serif, + system-ui, + sans-serif, + Apple Color Emoji, + Segoe UI Emoji, + Segoe UI Symbol, + Noto Color Emoji; + font-feature-settings: normal; + font-variation-settings: normal; + -moz-tab-size: 4; + tab-size: 4; + -webkit-tap-highlight-color: transparent; +} +body { + line-height: inherit; + margin: 0; +} +hr { + border-top-width: 1px; + color: inherit; + height: 0; +} +abbr:where([title]) { + text-decoration: underline dotted; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} +a { + color: inherit; + text-decoration: inherit; +} +b, +strong { + font-weight: bolder; +} +code, +kbd, +pre, +samp { + font-family: + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + Liberation Mono, + Courier New, + monospace; + font-feature-settings: normal; + font-size: 1em; + font-variation-settings: normal; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sub { + bottom: -0.25em; +} +sup { + top: -0.5em; +} +table { + border-collapse: collapse; + border-color: inherit; + text-indent: 0; +} +button, +input, +optgroup, +select, +textarea { + color: inherit; + font-family: inherit; + font-feature-settings: inherit; + font-size: 100%; + font-variation-settings: inherit; + font-weight: inherit; + letter-spacing: inherit; + line-height: inherit; + margin: 0; + padding: 0; +} +button, +select { + text-transform: none; +} +button, +input:where([type="button"]), +input:where([type="reset"]), +input:where([type="submit"]) { + -webkit-appearance: button; + background-color: transparent; + background-image: none; +} +:-moz-focusring { + outline: auto; +} +:-moz-ui-invalid { + box-shadow: none; +} +progress { + vertical-align: baseline; +} +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} +[type="search"] { + -webkit-appearance: textfield; + outline-offset: -2px; +} +::-webkit-search-decoration { + -webkit-appearance: none; +} +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} +summary { + display: list-item; +} +blockquote, +dd, +dl, +figure, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +p, +pre { + margin: 0; +} +fieldset { + margin: 0; +} +fieldset, +legend { + padding: 0; +} +menu, +ol, +ul { + list-style: none; + margin: 0; + padding: 0; +} +dialog { + padding: 0; +} +textarea { + resize: vertical; +} +input::placeholder, +textarea::placeholder { + color: #9ca3af; + opacity: 1; +} +[role="button"], +button { + cursor: pointer; +} +:disabled { + cursor: default; +} +audio, +canvas, +embed, +iframe, +img, +object, +svg, +video { + display: block; + vertical-align: middle; +} +img, +video { + height: auto; + max-width: 100%; +} +[hidden] { + display: none; +} +*, +:after, +:before { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgba(59, 130, 246, 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgba(59, 130, 246, 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} +#webcrumbs .mt-2 { + margin-top: 8px; +} +#webcrumbs .mt-4 { + margin-top: 16px; +} +#webcrumbs .mt-6 { + margin-top: 24px; +} +#webcrumbs .flex { + display: flex; +} +#webcrumbs .h-\[120px\] { + height: 120px; +} +#webcrumbs .w-\[120px\] { + width: 120px; +} +#webcrumbs .w-\[200px\] { + width: 200px; +} +#webcrumbs .w-\[500px\] { + width: 500px; +} +#webcrumbs .flex-1 { + flex: 1 1 0%; +} +#webcrumbs .flex-row { + flex-direction: row; +} +#webcrumbs .flex-col { + flex-direction: column; +} +#webcrumbs .items-center { + align-items: center; +} +#webcrumbs .justify-between { + justify-content: space-between; +} +#webcrumbs .gap-2 { + gap: 8px; +} +#webcrumbs .gap-4 { + gap: 16px; +} +#webcrumbs .rounded-full { + border-radius: 9999px; +} +#webcrumbs .rounded-lg { + border-radius: 24px; +} +#webcrumbs .border-b { + border-bottom-width: 1px; +} +#webcrumbs .border-neutral-300 { + --tw-border-opacity: 1; + border-color: rgb(202 202 202 / var(--tw-border-opacity)); +} +#webcrumbs .bg-neutral-200 { + --tw-bg-opacity: 1; + background-color: rgb(224 224 224 / var(--tw-bg-opacity)); +} +#webcrumbs .bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} +#webcrumbs .object-cover { + object-fit: cover; +} +#webcrumbs .p-4 { + padding: 16px; +} +#webcrumbs .p-6 { + padding: 24px; +} +#webcrumbs .py-2 { + padding-bottom: 8px; + padding-top: 8px; +} +#webcrumbs .text-center { + text-align: center; +} +#webcrumbs .font-title { + font-family: + Lato, + ui-sans-serif, + system-ui, + sans-serif, + Apple Color Emoji, + Segoe UI Emoji, + Segoe UI Symbol, + Noto Color Emoji; +} +#webcrumbs .text-lg { + font-size: 18px; + line-height: 27px; +} +#webcrumbs .text-sm { + font-size: 14px; + line-height: 21px; +} +#webcrumbs .text-neutral-950 { + --tw-text-opacity: 1; + color: rgb(40 40 40 / var(--tw-text-opacity)); +} +#webcrumbs .shadow-lg { + --tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -4px rgba(0, 0, 0, 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), + 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +#webcrumbs { + font-family: Open Sans !important; + font-size: 16px !important; +} +#webcrumbs :is(.bg-neutral-200) { + color: rgba(0, 0, 0, 0.9) !important; +} + +#user { + line-height: inherit; + padding: 24px; + display: flex; + flex-direction: column; + min-width: 80vw; + min-height: 100vh; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #ffffff, #d4d4d4); +} \ No newline at end of file diff --git a/src/main/resources/static/css/admin/list.css b/src/main/resources/static/css/admin/list.css index f890baf4..13c0433e 100644 --- a/src/main/resources/static/css/admin/list.css +++ b/src/main/resources/static/css/admin/list.css @@ -1,6 +1,4 @@ -@import url(https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200); - -@import url(https://fonts.googleapis.com/css2?family=Nunito&display=swap); +@import url(https://fonts.googleapis.com/css2?family=Lato&display=swap); @import url(https://fonts.googleapis.com/css2?family=Open+Sans&display=swap); @@ -11,53 +9,41 @@ border: 0 solid #e5e7eb; box-sizing: border-box; } - :after, :before { --tw-content: ""; } - :host, html { line-height: 1.5; -webkit-text-size-adjust: 100%; - font-family: Nunito, - ui-sans-serif, - system-ui, - sans-serif, - Apple Color Emoji, - Segoe UI Emoji, - Segoe UI Symbol, - Noto Color Emoji; + font-family: + Open Sans, + ui-sans-serif, + system-ui, + sans-serif, + Apple Color Emoji, + Segoe UI Emoji, + Segoe UI Symbol, + Noto Color Emoji; font-feature-settings: normal; font-variation-settings: normal; -moz-tab-size: 4; tab-size: 4; -webkit-tap-highlight-color: transparent; - --tw-shadow-color: rgba(0, 0, 0, 0.5); } - body { line-height: inherit; margin: 0; } - -main { - width: 100%; - justify-self: center; - gap: 70px; -} - hr { border-top-width: 1px; color: inherit; height: 0; } - abbr:where([title]) { text-decoration: underline dotted; } - h1, h2, h3, @@ -67,38 +53,34 @@ h6 { font-size: inherit; font-weight: inherit; } - a { color: inherit; text-decoration: inherit; } - b, strong { font-weight: bolder; } - code, kbd, pre, samp { - font-family: ui-monospace, - SFMono-Regular, - Menlo, - Monaco, - Consolas, - Liberation Mono, - Courier New, - monospace; + font-family: + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + Liberation Mono, + Courier New, + monospace; font-feature-settings: normal; font-size: 1em; font-variation-settings: normal; } - small { font-size: 80%; } - sub, sup { font-size: 75%; @@ -106,21 +88,17 @@ sup { position: relative; vertical-align: baseline; } - sub { bottom: -0.25em; } - sup { top: -0.5em; } - table { border-collapse: collapse; border-color: inherit; text-indent: 0; } - button, input, optgroup, @@ -137,12 +115,10 @@ textarea { margin: 0; padding: 0; } - button, select { text-transform: none; } - button, input:where([type="button"]), input:where([type="reset"]), @@ -151,42 +127,33 @@ input:where([type="submit"]) { background-color: transparent; background-image: none; } - :-moz-focusring { outline: auto; } - :-moz-ui-invalid { box-shadow: none; } - progress { vertical-align: baseline; } - ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; } - [type="search"] { -webkit-appearance: textfield; outline-offset: -2px; } - ::-webkit-search-decoration { -webkit-appearance: none; } - ::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; } - summary { display: list-item; } - blockquote, dd, dl, @@ -202,16 +169,13 @@ p, pre { margin: 0; } - fieldset { margin: 0; } - fieldset, legend { padding: 0; } - menu, ol, ul { @@ -219,30 +183,24 @@ ul { margin: 0; padding: 0; } - dialog { padding: 0; } - textarea { resize: vertical; } - input::placeholder, textarea::placeholder { color: #9ca3af; opacity: 1; } - [role="button"], button { cursor: pointer; } - :disabled { cursor: default; } - audio, canvas, embed, @@ -254,17 +212,14 @@ video { display: block; vertical-align: middle; } - img, video { height: auto; max-width: 100%; } - [hidden] { display: none; } - *, :after, :before { @@ -320,7 +275,6 @@ video { --tw-contain-paint: ; --tw-contain-style: ; } - ::backdrop { --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; @@ -374,299 +328,197 @@ video { --tw-contain-paint: ; --tw-contain-style: ; } - -.mx-2 { - margin-left: 8px; - margin-right: 8px; -} - -.mx-auto { - margin-left: auto; - margin-right: auto; -} - -.mb-1 { - margin-bottom: 4px; -} - -.mb-4 { - margin-bottom: 16px; -} - -.mb-6 { +#webcrumbs .mb-6 { margin-bottom: 24px; } - -.mb-8 { - margin-bottom: 32px; -} - -.ml-6 { - margin-left: 24px; -} - -.mt-6 { - margin-top: 24px; -} -.mt-7 { - margin-top: 15px; -} - -.flex { +#webcrumbs .flex { display: flex; } - -.h-4 { - height: 16px; -} - -.h-\[100px\] { - height: 100px; -} - -.min-h-\[800px\] { - min-height: 800px; +#webcrumbs .w-\[1200px\] { + width: 1200px; } - -.w-\[100px\] { - width: 100px; +#webcrumbs .w-\[300px\] { + width: 300px; } - -.w-\[1200px\] { - width: 1200px; +#webcrumbs .w-full { + width: 100%; } - -.w-\[1440px\] { - width: 1440px; +#webcrumbs .w-max { + width: 100vw; } - -.w-\[250px\] { - width: 250px; +#webcrumbs .w-img{ + width: 10px; } - -.w-full { - width: 100%; +#webcrumbs .h-img{ + width: 10px; } -.flex-1 { +#webcrumbs .flex-1 { flex: 1 1 0%; } - -.flex-row { +#webcrumbs .flex-row { flex-direction: row; } - -.flex-col { +#webcrumbs .flex-col { flex-direction: column; } - -.items-center { +#webcrumbs .items-center { align-items: center; } - -.justify-center { - justify-content: center; -} - -.justify-between { +#webcrumbs .justify-between { justify-content: space-between; } - -.gap-4 { +#webcrumbs .gap-4 { gap: 16px; } - -:is(.space-x-2 > :not([hidden]) ~ :not([hidden])) { - --tw-space-x-reverse: 0; - margin-left: calc(8px * (1 - var(--tw-space-x-reverse))); - margin-right: calc(8px * var(--tw-space-x-reverse)); +#webcrumbs .gap-6 { + gap: 24px; } - -:is(.space-x-4 > :not([hidden]) ~ :not([hidden])) { - --tw-space-x-reverse: 0; - margin-left: calc(16px * (1 - var(--tw-space-x-reverse))); - margin-right: calc(16px * var(--tw-space-x-reverse)); -} - -:is(.space-y-4 > :not([hidden]) ~ :not([hidden])) { - --tw-space-y-reverse: 0; - margin-bottom: calc(16px * var(--tw-space-y-reverse)); - margin-top: calc(16px * (1 - var(--tw-space-y-reverse))); -} - -.rounded-full { - border-radius: 9999px; -} - -.rounded-lg { - border-radius: 16px; +#webcrumbs .overflow-x-auto { + overflow-x: auto; } - -.rounded-md { - border-radius: 12px; +#webcrumbs .rounded-lg { + border-radius: 24px; } - -.border { - border-width: 1px; +#webcrumbs .rounded-md { + border-radius: 18px; } - -.border-l { - border-left-width: 1px; +#webcrumbs .bg-black { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); } - -.border-neutral-300 { - --tw-border-opacity: 1; - border-color: rgb(117 138 170 / var(--tw-border-opacity)); +#webcrumbs .bg-neutral-200 { + --tw-bg-opacity: 1; + background-color: rgb(224 224 224 / var(--tw-bg-opacity)); } - -.bg-neutral-100 { +#webcrumbs .bg-neutral-50 { --tw-bg-opacity: 1; - background-color: rgb(243 242 242 / var(--tw-bg-opacity)); + background-color: rgb(247 247 247 / var(--tw-bg-opacity)); } - -.bg-neutral-200 { +#webcrumbs .bg-neutral-800 { --tw-bg-opacity: 1; - background-color: rgb(235 234 234 / var(--tw-bg-opacity)); + background-color: rgb(84 84 84 / var(--tw-bg-opacity)); } - -.bg-neutral-300 { +#webcrumbs .bg-neutral-950 { --tw-bg-opacity: 1; - background-color: rgb(228 227 227 / var(--tw-bg-opacity)); + background-color: rgb(40 40 40 / var(--tw-bg-opacity)); } - -.bg-neutral-950 { +#webcrumbs .bg-primary-500 { --tw-bg-opacity: 1; - background-color: #486284; + background-color: rgb(115 65 255 / var(--tw-bg-opacity)); } - -.bg-white { +#webcrumbs .bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); } - -.p-2 { - padding: 8px; +#webcrumbs .p-4 { + padding: 16px; } - -.p-5 { - padding: 20px; +#webcrumbs .p-6 { + padding: 24px; } - -.p-8 { - padding: 32px; -} - -.px-4 { +#webcrumbs .px-4 { padding-left: 16px; padding-right: 16px; } - -.px-6 { +#webcrumbs .px-6 { padding-left: 24px; padding-right: 24px; } - -.py-1 { - padding-bottom: 4px; - padding-top: 4px; -} - -.py-10 { - padding-bottom: 40px; - padding-top: 40px; -} - -.py-2 { +#webcrumbs .py-2 { padding-bottom: 8px; padding-top: 8px; } - -.py-4 { +#webcrumbs .py-4 { padding-bottom: 16px; padding-top: 16px; } - -.font-title { - font-family: Nunito, - ui-sans-serif, - system-ui, - sans-serif, - Apple Color Emoji, - Segoe UI Emoji, - Segoe UI Symbol, - Noto Color Emoji; -} - -.text-lg { +#webcrumbs .text-left { + text-align: left; +} +#webcrumbs .font-title { + font-family: + Lato, + ui-sans-serif, + system-ui, + sans-serif, + Apple Color Emoji, + Segoe UI Emoji, + Segoe UI Symbol, + Noto Color Emoji; +} +#webcrumbs .text-lg { font-size: 18px; line-height: 27px; } - -.text-sm { - font-size: 14px; - line-height: 21px; -} - -.text-xl { +#webcrumbs .text-xl { font-size: 20px; line-height: 28px; } - -.text-neutral-50 { +#webcrumbs .text-neutral-300 { --tw-text-opacity: 1; - color: rgb(250 249 249 / var(--tw-text-opacity)); + color: rgb(202 202 202 / var(--tw-text-opacity)); } - -.text-neutral-600 { +#webcrumbs .text-neutral-50 { --tw-text-opacity: 1; - color: rgb(100 100 100 / var(--tw-text-opacity)); + color: rgb(247 247 247 / var(--tw-text-opacity)); } - -.text-neutral-950 { +#webcrumbs .text-neutral-950 { --tw-text-opacity: 1; - color: #486284; - /*color: rgb(10 10 10 / var(--tw-text-opacity));*/ + color: rgb(40 40 40 / var(--tw-text-opacity)); } - -.shadow { - --tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1); - --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), - 0 1px 2px -1px var(--tw-shadow-color); +#webcrumbs .text-primary-50 { + --tw-text-opacity: 1; + color: rgb(243 241 255 / var(--tw-text-opacity)); +} +#webcrumbs .text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} +#webcrumbs .shadow-lg { + --tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -4px rgba(0, 0, 0, 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), + 0 4px 6px -4px var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } - -:is(.bg-neutral-100) { - color: rgba(0, 0, 0, 0.9) !important; +#webcrumbs { + font-family: Open Sans !important; + font-size: 16px !important; } - -:is(.bg-neutral-200) { +#webcrumbs :is(.bg-neutral-50) { color: rgba(0, 0, 0, 0.9) !important; } - -:is(.bg-neutral-300) { +#webcrumbs :is(.bg-neutral-200) { color: rgba(0, 0, 0, 0.9) !important; } - -.text-white { - color: #ffffff; -} -.sidebar { - width: 250px; - height: 150vh; - background-color: #f8f9fa; - position: fixed; - top: 80px; - left: 0; - padding-top: 20px; -} -.sidebar a { - padding: 10px 15px; - text-decoration: none; - display: block; - color: #333; +#webcrumbs :is(.bg-neutral-800) { + color: hsla(0, 0%, 100%, 0.9) !important; +} +#webcrumbs :is(.bg-neutral-950) { + color: hsla(0, 0%, 100%, 0.9) !important; +} +#webcrumbs :is(.bg-primary-500) { + color: hsla(0, 0%, 100%, 0.9) !important; } -.sidebar a:hover { - background-color: #e9ecef; - color: #000; +#webcrumbs .hover\:bg-neutral-100:hover { + --tw-bg-opacity: -800; + background-color: rgb(245 245 245 / var(--tw-bg-opacity)); +} +#webcrumbs .hover\:bg-neutral-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(150 150 150 / var(--tw-bg-opacity)); +} +body { + line-height: inherit; + padding: 24px; + display: flex; + flex-direction: column; + min-width: 100vw; + min-height: 100vh; + height: 300px; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #ffffff, #d4d4d4); } \ No newline at end of file diff --git a/src/main/resources/static/css/admin/login.css b/src/main/resources/static/css/admin/login.css new file mode 100644 index 00000000..32ca4876 --- /dev/null +++ b/src/main/resources/static/css/admin/login.css @@ -0,0 +1,63 @@ +body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: auto; + background-color: #2C2C2C; + font-family: Arial, sans-serif; +} + + +.signup-container { + width: 300px; + padding: 20px; + background: #ffffff; + border-radius: 10px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + align-items: center; +} + +h2 { + margin-bottom: 50px; + font-size: 24px; + font-weight: bold; +} + +.form-group { + width: 100%; + margin-bottom: 15px; +} + + +.form-group label { + display: block; + margin-bottom: 5px; + font-size: 14px; +} + +.form-group input { + width: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + box-sizing: border-box; +} + +.sign-in-btn { + width: 100%; + padding: 10px; + margin: 0 auto; + border: none; + border-radius: 5px; + background-color: #333; + color: #fff; + font-size: 16px; + cursor: pointer; +} + +.sign-in-btn:hover { + background-color: #555; +} diff --git a/src/main/resources/static/css/admin/main.css b/src/main/resources/static/css/admin/main.css new file mode 100644 index 00000000..46054f68 --- /dev/null +++ b/src/main/resources/static/css/admin/main.css @@ -0,0 +1,485 @@ +@import url(https://fonts.googleapis.com/css2?family=Lato&display=swap); + +@import url(https://fonts.googleapis.com/css2?family=Open+Sans&display=swap); + +/*! tailwindcss v3.4.11 | MIT License | https://tailwindcss.com*/ +*, +:after, +:before { + border: 0 solid #e5e7eb; + box-sizing: border-box; +} +:after, +:before { + --tw-content: ""; +} +:host, +html { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + font-family: + Open Sans, + ui-sans-serif, + system-ui, + sans-serif, + Apple Color Emoji, + Segoe UI Emoji, + Segoe UI Symbol, + Noto Color Emoji; + font-feature-settings: normal; + font-variation-settings: normal; + -moz-tab-size: 4; + tab-size: 4; + -webkit-tap-highlight-color: transparent; +} +body { + line-height: inherit; + margin: 0; +} +hr { + border-top-width: 1px; + color: inherit; + height: 0; +} +abbr:where([title]) { + text-decoration: underline dotted; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} +a { + color: inherit; + text-decoration: inherit; +} +b, +strong { + font-weight: bolder; +} +code, +kbd, +pre, +samp { + font-family: + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + Liberation Mono, + Courier New, + monospace; + font-feature-settings: normal; + font-size: 1em; + font-variation-settings: normal; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sub { + bottom: -0.25em; +} +sup { + top: -0.5em; +} +table { + border-collapse: collapse; + border-color: inherit; + text-indent: 0; +} +button, +input, +optgroup, +select, +textarea { + color: inherit; + font-family: inherit; + font-feature-settings: inherit; + font-size: 100%; + font-variation-settings: inherit; + font-weight: inherit; + letter-spacing: inherit; + line-height: inherit; + margin: 0; + padding: 0; +} +button, +select { + text-transform: none; +} +button, +input:where([type="button"]), +input:where([type="reset"]), +input:where([type="submit"]) { + -webkit-appearance: button; + background-color: transparent; + background-image: none; +} +:-moz-focusring { + outline: auto; +} +:-moz-ui-invalid { + box-shadow: none; +} +progress { + vertical-align: baseline; +} +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} +[type="search"] { + -webkit-appearance: textfield; + outline-offset: -2px; +} +::-webkit-search-decoration { + -webkit-appearance: none; +} +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} +summary { + display: list-item; +} +blockquote, +dd, +dl, +figure, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +p, +pre { + margin: 0; +} +fieldset { + margin: 0; +} +fieldset, +legend { + padding: 0; +} +menu, +ol, +ul { + list-style: none; + margin: 0; + padding: 0; +} +dialog { + padding: 0; +} +textarea { + resize: vertical; +} +input::placeholder, +textarea::placeholder { + color: #9ca3af; + opacity: 1; +} +[role="button"], +button { + cursor: pointer; +} +:disabled { + cursor: default; +} +audio, +canvas, +embed, +iframe, +img, +object, +svg, +video { + display: block; + vertical-align: middle; +} +img, +video { + height: auto; + max-width: 100%; +} +[hidden] { + display: none; +} +*, +:after, +:before { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgba(59, 130, 246, 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgba(59, 130, 246, 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} +#webcrumbs .mx-auto { + margin-left: auto; + margin-right: auto; +} +#webcrumbs .mb-6 { + margin-bottom: 24px; +} +#webcrumbs .flex { + display: flex; +} +#webcrumbs .h-\[40px\] { + height: 40px; +} +#webcrumbs .h-\[50px\] { + height: 50px; +} +#webcrumbs .min-h-\[600px\] { + min-height: 1000px; +} +#webcrumbs .w-\[100px\] { + width: 100px; +} +#webcrumbs .w-\[1200px\] { + width: 1200px; +} +#webcrumbs .w-\[330px\] { + width: 330px; +} +#webcrumbs .w-full { + width: 100%; +} +#webcrumbs .w-max { + width:100vw +} +#webcrumbs .flex-row { + flex-direction: row; +} +#webcrumbs .items-start { + align-items: flex-start; +} +#webcrumbs .items-center { + align-items: center; +} +#webcrumbs .justify-center { + justify-content: center; +} +#webcrumbs .justify-between { + justify-content: space-between; +} +#webcrumbs .gap-4 { + gap: 16px; +} +#webcrumbs :is(.space-x-4 > :not([hidden]) ~ :not([hidden])) { + --tw-space-x-reverse: 0; + margin-left: calc(16px * (1 - var(--tw-space-x-reverse))); + margin-right: calc(16px * var(--tw-space-x-reverse)); +} +#webcrumbs :is(.space-y-4 > :not([hidden]) ~ :not([hidden])) { + --tw-space-y-reverse: 0; + margin-bottom: calc(16px * var(--tw-space-y-reverse)); + margin-top: calc(16px * (1 - var(--tw-space-y-reverse))); +} +#webcrumbs .rounded-lg { + border-radius: 24px; +} +#webcrumbs .rounded-md { + border-radius: 18px; +} +#webcrumbs .border-2 { + border-width: 2px; +} +#webcrumbs .border-b { + border-bottom-width: 1px; +} +#webcrumbs .border-neutral-700 { + --tw-border-opacity: 1; + border-color: rgb(103 103 103 / var(--tw-border-opacity)); +} +#webcrumbs .border-red-500 { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity)); +} +#webcrumbs .bg-black { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); +} +#webcrumbs .bg-neutral-200 { + --tw-bg-opacity: 1; + background-color: rgb(224 224 224 / var(--tw-bg-opacity)); +} +#webcrumbs .bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} +#webcrumbs .p-6 { + padding: 24px; +} +#webcrumbs .px-6 { + padding-left: 24px; + padding-right: 24px; +} +#webcrumbs .py-4 { + padding-bottom: 16px; + padding-top: 16px; +} +#webcrumbs .pt-8 { + padding-top: 32px; +} +#webcrumbs .font-title { + font-family: + Lato, + ui-sans-serif, + system-ui, + sans-serif, + Apple Color Emoji, + Segoe UI Emoji, + Segoe UI Symbol, + Noto Color Emoji; +} +#webcrumbs .text-2xl { + font-size: 24px; + line-height: 31.200000000000003px; +} +#webcrumbs .text-xl { + font-size: 20px; + line-height: 28px; +} +#webcrumbs .text-black { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} +#webcrumbs .text-neutral-50 { + --tw-text-opacity: 1; + color: rgb(247 247 247 / var(--tw-text-opacity)); +} +#webcrumbs .text-neutral-900 { + --tw-text-opacity: 1; + color: rgb(70 70 70 / var(--tw-text-opacity)); +} +#webcrumbs .text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} +#webcrumbs { + font-family: Open Sans !important; + font-size: 16px !important; +} +#webcrumbs :is(.bg-neutral-200) { + color: rgba(0, 0, 0, 0.9) !important; +} + +#webcrumbs .hover\:bg-neutral-100:hover { + --tw-bg-opacity: -800; + background-color: rgb(245 245 245 / var(--tw-bg-opacity)); +} \ No newline at end of file diff --git a/src/main/resources/static/css/admin/withdrawal.css b/src/main/resources/static/css/admin/withdrawal.css new file mode 100644 index 00000000..d9966eff --- /dev/null +++ b/src/main/resources/static/css/admin/withdrawal.css @@ -0,0 +1,525 @@ +@import url(https://fonts.googleapis.com/css2?family=Lato&display=swap); + +@import url(https://fonts.googleapis.com/css2?family=Open+Sans&display=swap); + +/*! tailwindcss v3.4.11 | MIT License | https://tailwindcss.com*/ +*, +:after, +:before { + border: 0 solid #e5e7eb; + box-sizing: border-box; +} +:after, +:before { + --tw-content: ""; +} +:host, +html { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + font-family: + Open Sans, + ui-sans-serif, + system-ui, + sans-serif, + Apple Color Emoji, + Segoe UI Emoji, + Segoe UI Symbol, + Noto Color Emoji; + font-feature-settings: normal; + font-variation-settings: normal; + -moz-tab-size: 4; + tab-size: 4; + -webkit-tap-highlight-color: transparent; +} +body { + line-height: inherit; + margin: 0; +} +hr { + border-top-width: 1px; + color: inherit; + height: 0; +} +abbr:where([title]) { + text-decoration: underline dotted; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} +a { + color: inherit; + text-decoration: inherit; +} +b, +strong { + font-weight: bolder; +} +code, +kbd, +pre, +samp { + font-family: + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + Liberation Mono, + Courier New, + monospace; + font-feature-settings: normal; + font-size: 1em; + font-variation-settings: normal; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sub { + bottom: -0.25em; +} +sup { + top: -0.5em; +} +table { + border-collapse: collapse; + border-color: inherit; + text-indent: 0; +} +button, +input, +optgroup, +select, +textarea { + color: inherit; + font-family: inherit; + font-feature-settings: inherit; + font-size: 100%; + font-variation-settings: inherit; + font-weight: inherit; + letter-spacing: inherit; + line-height: inherit; + margin: 0; + padding: 0; +} +button, +select { + text-transform: none; +} +button, +input:where([type="button"]), +input:where([type="reset"]), +input:where([type="submit"]) { + -webkit-appearance: button; + background-color: transparent; + background-image: none; +} +:-moz-focusring { + outline: auto; +} +:-moz-ui-invalid { + box-shadow: none; +} +progress { + vertical-align: baseline; +} +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} +[type="search"] { + -webkit-appearance: textfield; + outline-offset: -2px; +} +::-webkit-search-decoration { + -webkit-appearance: none; +} +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} +summary { + display: list-item; +} +blockquote, +dd, +dl, +figure, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +p, +pre { + margin: 0; +} +fieldset { + margin: 0; +} +fieldset, +legend { + padding: 0; +} +menu, +ol, +ul { + list-style: none; + margin: 0; + padding: 0; +} +dialog { + padding: 0; +} +textarea { + resize: vertical; +} +input::placeholder, +textarea::placeholder { + color: #9ca3af; + opacity: 1; +} +[role="button"], +button { + cursor: pointer; +} +:disabled { + cursor: default; +} +audio, +canvas, +embed, +iframe, +img, +object, +svg, +video { + display: block; + vertical-align: middle; +} +img, +video { + height: auto; + max-width: 100%; +} +[hidden] { + display: none; +} +*, +:after, +:before { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgba(59, 130, 246, 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgba(59, 130, 246, 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} +#webcrumbs .mb-6 { + margin-bottom: 24px; +} +#webcrumbs .flex { + display: flex; +} +#webcrumbs .w-\[1200px\] { + width: 1200px; +} +#webcrumbs .w-\[300px\] { + width: 300px; +} +#webcrumbs .w-full { + width: 100%; +} +#webcrumbs .w-max { + width: 100vw; +} +#webcrumbs .w-img{ + width: 10px; +} +#webcrumbs .h-img{ + width: 10px; +} +#webcrumbs .h-max{ + height: 800px; +} +#webcrumbs .flex-1 { + flex: 1 1 0%; +} +#webcrumbs .flex-row { + flex-direction: row; +} +#webcrumbs .flex-col { + flex-direction: column; +} +#webcrumbs .items-center { + align-items: center; +} +#webcrumbs .justify-between { + justify-content: space-between; +} +#webcrumbs .gap-4 { + gap: 16px; +} +#webcrumbs .gap-6 { + gap: 24px; +} +#webcrumbs .overflow-x-auto { + overflow-x: auto; +} +#webcrumbs .rounded-lg { + border-radius: 24px; +} +#webcrumbs .rounded-md { + border-radius: 18px; +} +#webcrumbs .bg-black { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); +} +#webcrumbs .bg-neutral-200 { + --tw-bg-opacity: 1; + background-color: rgb(224 224 224 / var(--tw-bg-opacity)); +} +#webcrumbs .bg-neutral-50 { + --tw-bg-opacity: 1; + background-color: rgb(247 247 247 / var(--tw-bg-opacity)); +} +#webcrumbs .bg-neutral-800 { + --tw-bg-opacity: 1; + background-color: rgb(84 84 84 / var(--tw-bg-opacity)); +} +#webcrumbs .bg-neutral-950 { + --tw-bg-opacity: 1; + background-color: rgb(40 40 40 / var(--tw-bg-opacity)); +} +#webcrumbs .bg-primary-500 { + --tw-bg-opacity: 1; + background-color: rgb(115 65 255 / var(--tw-bg-opacity)); +} +#webcrumbs .bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} +#webcrumbs .p-4 { + padding: 16px; +} +#webcrumbs .p-6 { + padding: 24px; +} +#webcrumbs .px-4 { + padding-left: 16px; + padding-right: 16px; +} +#webcrumbs .px-6 { + padding-left: 24px; + padding-right: 24px; +} +#webcrumbs .py-2 { + padding-bottom: 8px; + padding-top: 8px; +} +#webcrumbs .py-4 { + padding-bottom: 16px; + padding-top: 16px; +} +#webcrumbs .text-left { + text-align: left; +} +#webcrumbs .font-title { + font-family: + Lato, + ui-sans-serif, + system-ui, + sans-serif, + Apple Color Emoji, + Segoe UI Emoji, + Segoe UI Symbol, + Noto Color Emoji; +} +#webcrumbs .text-lg { + font-size: 18px; + line-height: 27px; +} +#webcrumbs .text-xl { + font-size: 20px; + line-height: 28px; +} +#webcrumbs .text-neutral-300 { + --tw-text-opacity: 1; + color: rgb(202 202 202 / var(--tw-text-opacity)); +} +#webcrumbs .text-neutral-50 { + --tw-text-opacity: 1; + color: rgb(247 247 247 / var(--tw-text-opacity)); +} +#webcrumbs .text-neutral-950 { + --tw-text-opacity: 1; + color: rgb(40 40 40 / var(--tw-text-opacity)); +} +#webcrumbs .text-primary-50 { + --tw-text-opacity: 1; + color: rgb(243 241 255 / var(--tw-text-opacity)); +} +#webcrumbs .text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} +#webcrumbs .shadow-lg { + --tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -4px rgba(0, 0, 0, 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), + 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} +#webcrumbs { + font-family: Open Sans !important; + font-size: 16px !important; +} +#webcrumbs :is(.bg-neutral-50) { + color: rgba(0, 0, 0, 0.9) !important; +} +#webcrumbs :is(.bg-neutral-200) { + color: rgba(0, 0, 0, 0.9) !important; +} +#webcrumbs :is(.bg-neutral-800) { + color: hsla(0, 0%, 100%, 0.9) !important; +} +#webcrumbs :is(.bg-neutral-950) { + color: hsla(0, 0%, 100%, 0.9) !important; +} +#webcrumbs :is(.bg-primary-500) { + color: hsla(0, 0%, 100%, 0.9) !important; +} +#webcrumbs .hover\:bg-neutral-100:hover { + --tw-bg-opacity: -800; + background-color: rgb(245 245 245 / var(--tw-bg-opacity)); +} +#webcrumbs .hover\:bg-neutral-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(150 150 150 / var(--tw-bg-opacity)); +} +body { + line-height: inherit; + padding: 24px; + display: flex; + flex-direction: column; + min-width: 100vw; + height: 100vh; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #ffffff, #d4d4d4); +} \ No newline at end of file diff --git a/src/main/resources/static/css/crossroad/signal.css b/src/main/resources/static/css/crossroad/signal.css new file mode 100644 index 00000000..7088dc05 --- /dev/null +++ b/src/main/resources/static/css/crossroad/signal.css @@ -0,0 +1,25 @@ +.circle{ + width: 50px; + height: 50px; + border-radius: 50%; + background-color: rgb(252, 83, 83); + text-align: center; + line-height: 3; + margin: 10px; +} + +.suggestions-list { + position: absolute; /* 부모 요소 기준으로 위치 설정 */ + width: 20%; + max-height: 200px; /* 최대 높이 제한 */ + overflow-y: auto; /* 스크롤바 표시 */ + border: 1px solid #ccc; /* 테두리 */ + background-color: white; + z-index: 1000; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + visibility: hidden; /* 초기에는 숨김 */ +} + +.suggestions-list.show { + visibility: visible; /* 보이도록 설정 */ +} \ No newline at end of file diff --git a/src/main/resources/static/css/member/info.css b/src/main/resources/static/css/member/info.css index a1a1f872..22fa472d 100644 --- a/src/main/resources/static/css/member/info.css +++ b/src/main/resources/static/css/member/info.css @@ -15,4 +15,17 @@ .cancel { background-color: #1b2b40; +} + +.profile-img { + position: relative; + display: block; + width: 120px; + height: 120px; + margin: 20px auto; + border-radius: 50%; + background-color: #e0e0e0; + background-size: cover; + background-position: center; + object-fit: cover; } \ No newline at end of file diff --git a/src/main/resources/static/css/member/login.css b/src/main/resources/static/css/member/login.css new file mode 100644 index 00000000..60b07968 --- /dev/null +++ b/src/main/resources/static/css/member/login.css @@ -0,0 +1,166 @@ +body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: auto; + background-color: #f0f0f0; + font-family: Arial, sans-serif; + padding-top: 70px; +} + +/* 헤더 고정 위치 */ +header { + position: fixed; + top: 0; + left: 0; + width: 100%; + background-color: #fff; + z-index: 1000; /* 다른 요소들 위에 위치하도록 */ + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 헤더 아래 그림자 효과 */ +} + +.login-container { + width: 350px; + padding: 20px; + background: #ffffff; + border-radius: 10px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + align-items: center; +} + +form { + width: 90%; + margin: 3px; +} + +h2 { + text-align: center; + margin-bottom: 25px; + font-size: 24px; + font-weight: bold; +} + +.form-group { + width: 100%; + margin-bottom: 10px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-size: 14px; +} + +.form-group input { + width: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + box-sizing: border-box; +} + +.sign-in-btn { + width: 100%; + padding: 10px; + margin: 3px auto; + border: none; + border-radius: 5px; + background-color: #333; + color: #fff; + font-size: 16px; + cursor: pointer; +} + +.sign-in-btn:hover { + background-color: #555; +} + +.links { + display: flex; + justify-content: center; + width: 100%; + font-size: 12px; +} + +.links a { + text-decoration: none; + color: #333; +} + +.links a:hover { + text-decoration: underline; +} + +.options { + margin-top: 10px; + display: flex; + justify-content: space-between; + font-size: 14px; +} + +.divider { + display: flex; /* Flexbox 사용 */ + align-items: center; /* 세로 방향 가운데 정렬 */ + justify-content: center; /* 양옆 선의 균등 분배 */ + width: 100%; /* 부모 요소의 너비 꽉 채우기 */ + margin: 10px 0; +} + +.divider .line { + flex: 1; /* 양옆 선이 가능한 넓게 차지 */ + height: 1px; /* 선 두께 */ + background-color: #ddd; /* 선 색상 */ +} + +.divider span { + font-size: small; + margin: 0 10px; /* 선과 텍스트 사이 여백 */ + color: #b4b4b4; /* 텍스트 색상 */ + white-space: nowrap; /* 텍스트 줄바꿈 방지 */ +} + + +.oauth-buttons { + display: flex; + flex-direction: column; + width: 100%; +} + +.oauth-button { + display: flex; + align-items: center; + justify-content: center; + margin: 5px 0; + padding: 10px; + + border-radius: 5px; + border: 1px solid #ddd; + cursor: pointer; + background: white; + text-align: left; +} + +.oauth-button img { + height: 24px; /* 로고 높이 설정 */ + width: 24px; /* 로고 너비 설정 */ + object-fit: contain; /* 로고 비율 유지 */ + margin-right: 10px; /* 텍스트와의 간격 설정 */ +} + +.oauth-button.google { + background: white; +} + +.oauth-button.naver { + background: #2AC308; + color: white; +} + +.oauth-button.kakaotalk { + background: #FAE100; + color: #371D1E + +} \ No newline at end of file diff --git a/src/main/resources/static/css/member/signup.css b/src/main/resources/static/css/member/signup.css new file mode 100644 index 00000000..6c6a05c3 --- /dev/null +++ b/src/main/resources/static/css/member/signup.css @@ -0,0 +1,120 @@ +/* 전체 body 스타일 */ +body { + margin: 0; + font-family: Arial, sans-serif; + background-color: #f0f0f0; + padding-top: 70px; /* 헤더 높이에 맞춰 여백 추가 */ +} + +/* 헤더 고정 위치 */ +header { + position: fixed; + top: 0; + left: 0; + width: 100%; + background-color: #fff; + z-index: 1000; /* 다른 요소들 위에 위치하도록 */ + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 헤더 아래 그림자 효과 */ +} + +.signup-container { + width: 350px; + padding: 20px; + background: #ffffff; + border-radius: 10px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + margin: 50px auto 0; /* 80px로 상단 여백을 주어 헤더와 겹치지 않게 */ +} + +h2 { + text-align: center; + margin-bottom: 20px; + font-size: 24px; + font-weight: bold; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +.form-group input[type="text"], +.form-group input[type="email"], +.form-group input[type="password"] { + width: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + margin: 0 auto; +} + +.profile-preview { + position: relative; + display: block; + width: 120px; + height: 120px; + margin: 20px auto; + border-radius: 50%; + background-color: #e0e0e0; + background-size: cover; + background-position: center; +} + +.camera-btn { + position: absolute; + right: 0px; + bottom: 0px; + width: 30px; + height: 30px; + border-radius: 50%; + border: none; + background-color: white; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.camera-btn img { + width: 20px; + height: 20px; + object-fit: contain; +} + +.form-group input[type="file"] { + display: none; +} + +.signup-button { + width: 200px; + padding: 10px; + margin: 20px 50px 0px 50px; + border: none; + border-radius: 5px; + background-color: #333; + color: #fff; + font-size: 16px; + cursor: pointer; +} + +.signup-button:hover { + background-color: #555; +} + +.login-link { + display: block; + text-align: center; + margin-top: 10px; + color: #333; + text-decoration: none; +} + +.login-link:hover { + text-decoration: underline; +} diff --git a/src/main/resources/static/images/camera-icon.png b/src/main/resources/static/images/camera-icon.png new file mode 100644 index 00000000..ff8b1eef Binary files /dev/null and b/src/main/resources/static/images/camera-icon.png differ diff --git a/src/main/resources/static/images/socialLogo/google.png b/src/main/resources/static/images/socialLogo/google.png new file mode 100644 index 00000000..e27a9adb Binary files /dev/null and b/src/main/resources/static/images/socialLogo/google.png differ diff --git a/src/main/resources/static/images/socialLogo/kakaotalk.png b/src/main/resources/static/images/socialLogo/kakaotalk.png new file mode 100644 index 00000000..ab8d0835 Binary files /dev/null and b/src/main/resources/static/images/socialLogo/kakaotalk.png differ diff --git a/src/main/resources/static/images/socialLogo/naver.png b/src/main/resources/static/images/socialLogo/naver.png new file mode 100644 index 00000000..ab1f40ba Binary files /dev/null and b/src/main/resources/static/images/socialLogo/naver.png differ diff --git a/src/main/resources/static/js/crossroad/tMap.js b/src/main/resources/static/js/crossroad/tMap.js new file mode 100644 index 00000000..14e5da6b --- /dev/null +++ b/src/main/resources/static/js/crossroad/tMap.js @@ -0,0 +1,376 @@ + +let marker = null;// 마커 변수 +let abortController = new AbortController(); // API 요청 중 signal로 상태 변경 +let startMarker= null; +let endMarker= null; +let input = null; // 데이터 +let routeLayer= null; + +// 데이터 요청 상태 관리 +let isFetchingData = false; +let marker_id; +let minTime; + +const markers = {}; + + +// 지도 초기화 +var map = new Tmapv2.Map("map", { + center: new Tmapv2.LatLng(37.5665, 126.9780), // 지도 중심 좌표 (위도, 경도) + width: "100%", + height: "900px", + zoom: 12 // 줌 레벨 (값이 높을수록 확대) +}); + +map.addListener("click", function (event) { + latLng = null; // 값 초기화 + latLng = event.latLng; // 클릭한 위치의 위도, 경도 정보 + + console.log(latLng); + + if (startMarker && latLng) { + startMarker.setMap(null); // 기존 마커 제거 + } + + if(latLng){ + startMarker = new Tmapv2.Marker({ + position: latLng, + map: map + }); + } + +}); + +fetch('api/crossroads/marker') + .then(response=> response.json()) + .then(data=> { + // 데이터 순회하며 마커 생성 + data.forEach(crossroad => { + const lat = crossroad.mapCtptIntLat; + const lng = crossroad.mapCtptIntLot; + + const marker = new Tmapv2.Marker({ + position: new Tmapv2.LatLng(lat,lng), + map, + title: crossroad.itstNm + }); + + markers[crossroad.crossroadApiId]=marker; + + marker.addListener("click",()=> { + marker_id=crossroad.itstId + console.log("click"); + disableFetching(); + enableFetching(); + callSignalStatus(marker_id); + }); + }); + }); /** TODO: + * PROBLEM: marker 찍기는 map 객체에 별도로 상태를 저장해두기가 불가 + * SOLUTION: 서버에서 데이터의 상태를 유지하기 위한 기능 고민 + * WHY: marker 갯수가 많아지면 랜더링 시간이 과하게 길어질 수 있음 + */ + +// 데이터 갱신 비활성화 +function disableFetching() { + isFetchingData = false; + handleCountdown(); + console.log("Data fetching disabled."); +} + +// 데이터 갱신 활성화 +function enableFetching() { + isFetchingData = true; + console.log("Data fetching enabled."); +} + +// 데이터 저장 및 UI 반영 +function callSignalStatus(api_id){ + console.log(api_id); + fetch(`api/crossroads/state/${api_id}`,{ cache: "no-store" }) + .then(response => response.json()) + .then( data => { + updateUI(data); + }); +} + +// UI 업데이트 함수 +function updateUI(data) { + minTime=10000; + const directions = ["NW", "NT", "NE", "WT", "ET", "SW", "ST", "SE"]; + + console.log(data); + + directions.forEach((dir) => { + const stateKey = `${dir.toLowerCase()}PdsgStatNm`; + const timeKey = `${dir.toLowerCase()}PdsgRmdrCs`; + + if (!(stateKey in data[0]) || !(timeKey in data[0])) { + console.warn(`Missing data for direction: ${dir}`); + return; + } + + const state = data[0][stateKey]; + const time = data[0][timeKey]; + const circle = document.getElementById(dir); + + /* const statusColors = { + "stop-And-Remain": "red", + "protected-Movement-Allowed": "green", + "permissive-Movement-Allowed": "gray" + };*/ + + if (!state) { + circle.style.display = "none"; // null 상태는 숨김 + } else if(state){ + circle.style.display = "block"; + circle.style.backgroundColor = state; // 상태 색상 + minTime = Math.min(minTime, time)+10; // 최소 남은 시간 계산 + } + }); + + time(); +} + +// 1초마다 카운트다운 실행 +function handleCountdown() { + if (!isFetchingData) return; + + if (minTime > 0) { + minTime -= 10; // 남은 시간 감소 + } + + // 시간이 0이면 데이터 갱신 + if (minTime <= 0) { + console.log("Time expired during countdown. Fetching new data..."); + callSignalStatus(marker_id); + } + + time(); // UI 업데이트 +} + +function time(){ + // 최소 남은 시간 표시 + const timeSpan = document.querySelector(".text-body-secondary"); + timeSpan.textContent = `${minTime/10} s Left`; +} + +// 1초마다 카운트다운 실행 +setInterval(handleCountdown, 1000); + +// 검색 버튼 클릭 시 동작 +document.getElementById("search").addEventListener("click", async function() { + var address = document.getElementById("searchInput").value; // 입력된 주소 가져오기 + + if(address) { + // 기존 요청을 취소 + if (abortController) { + abortController.abort(); // 이전 요청 취소 + } + + //새로운 컨트롤러 생성 + abortController = new AbortController(); + + await searchAddress(address, abortController.signal); + } else { + alert("주소를 입력하세요!"); + } +}); + +// 주소 검색 함수 +async function searchAddress(address, signal) { + input = document.getElementById("searchInput").value; + // 검색할 주소 + const url = `https://apis.openapi.sk.com/tmap/pois?version=1&searchType=all&page=1&multiPoint=Y&searchtypCd=A&searchKeyword=${input}`; + + try { + const response = await fetch(url, { + method: "GET", + headers: { + "Accept": "application/json", + "appKey": "1bxEMLzGUg68a4EeRA5F14J5Vbgh6GWI3zLXabl9" + }, + signal: signal + }); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + + const data = await response.json(); + const pois = data.searchPoiInfo.pois.poi; + + if (pois && pois.length > 0) {// POI 이름만 추출 + updateSuggestions(pois); + } else { + updateSuggestions([]); + } + } catch (error) { + if (error.name === 'AbortError') { + // AbortError는 요청 취소이므로 그냥 무시하고 정상 처리 + console.log("검색이 취소되었습니다."); + } else { + console.error("Error during address search:", error); + } + updateSuggestions([]); + } +} + +//이벤트 핸들러 변수 저장 +const handleClick = (poi) => { + document.getElementById("searchInput").value = poi.name; // 클릭 시 입력 필드에 채우기 + findRoute(poi.frontLat, poi.frontLon); // 경로 찾기 +}; + +function updateSuggestions(pois) { + const suggestionsElement = document.getElementById("suggestions"); + suggestionsElement.innerHTML = ""; // 기존 추천어 목록 초기화 + + + if (pois.length === 0) { + suggestionsElement.style.display = "none"; + return; + } + + pois.forEach(poi => { + const div = document.createElement("div"); + div.textContent = poi.name; + + // 클릭 이벤트 리스너를 추가하기 전에 기존 이벤트 리스너 제거 + div.removeEventListener("click", handleClick); // 기존 이벤트 리스너 제거 (있다면) + suggestionsElement.style.visibility = "visible"; + + // 새로운 클릭 이벤트 리스너 추가 + div.addEventListener("click", () => handleClick(poi)); + suggestionsElement.appendChild(div); + + }); + + suggestionsElement.classList.add("show"); // 추천어 목록 표시 +} + +// 마커를 제거하는 함수 +function clearMarker() { + + if (startMarker) { + startMarker.setMap(null); // 지도에서 마커 제거 + startMarker = null; // 마커 변수 초기화 + endMarker.setMap(null); + endMarker = null; + } + +} + +document.getElementById("searchInput").addEventListener("input", function () { + const inputValue = searchInput.value.trim(); + if (inputValue === "") { + // 검색박스가 비었을 때 + clearMarker(); // 마커 제거 + } +}); + +// 경로 검색 +async function findRoute(endLat,endLon) { + + console.log(latLng); + + let startLat = latLng._lat; + let startLon = latLng._lng; + + document.getElementById("suggestions").style.visibility = "hidden";// 추천어 목록 숨김 + + if (!endLat || !endLon) { + alert("도착지를 선택하세요!"); + return; + } + + const url = `https://apis.openapi.sk.com/tmap/routes/pedestrian`; + const headers = { + "Accept": "application/json", + "appKey": "1bxEMLzGUg68a4EeRA5F14J5Vbgh6GWI3zLXabl9" + }; + const body = JSON.stringify({ + startX: startLon, + startY: startLat, + endX: endLon, + endY: endLat, + reqCoordType: "WGS84GEO", + resCoordType: "EPSG3857", + startName: "출발지", + endName: "도착지" + }); + + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json", ...headers }, + body + }); + const data = await response.json(); + const routeData = data.features; + + console.log(data); + + if (routeLayer) { + routeLayer.setMap(null); + } + + console.log(routeData); + + drawRoute(routeData, startLat, startLon, endLat, endLon); + } catch (error) { + console.error("길찾기 중 오류:", error); + } +} + +function convertToWGS84(x, y) { + var lon = (x / 20037508.34) * 180; + var lat = (y / 20037508.34) * 180; + lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180)) - Math.PI / 2); + + return { lat: lat, lon: lon }; +} // 더러운거 보지 마세요 -> 좌표계 변경 + +// 경로 그리기 +function drawRoute(routeData, startLat, startLon, endLat, endLon) { + + const path = routeData + .filter(item => item.geometry.type === "LineString") + .flatMap(item => item.geometry.coordinates.map(coord => { + // 좌표 변환 + const converted = convertToWGS84(coord[0], coord[1]); + + // 변환된 좌표 유효성 검사 + if (converted.lat === 0 && converted.lon === 0) { + console.warn("Invalid coordinates detected:", coord); + return null; // 잘못된 좌표는 제외 + } + + // 유효한 좌표를 LatLng 객체로 반환 + return new Tmapv2.LatLng(converted.lat, converted.lon); + }) + ) + .filter(Boolean); + + routeLayer = new Tmapv2.Polyline({ + path, + strokeColor: "#ff0000", + strokeWeight: 6, + map: map + }); + + addMarkers(startLat, startLon, endLat, endLon); +} + +// 마커 추가 +function addMarkers(startLat, startLon, endLat, endLon) { + + if (endMarker) endMarker.setMap(null); + + endMarker = new Tmapv2.Marker({ + position: new Tmapv2.LatLng(endLat, endLon), + map, + title: "도착지" + }); + +} diff --git a/src/main/resources/templates/admin/detail.html b/src/main/resources/templates/admin/detail.html index f18cbcc8..863a3bf1 100644 --- a/src/main/resources/templates/admin/detail.html +++ b/src/main/resources/templates/admin/detail.html @@ -2,27 +2,67 @@ + 사용자 상세 조회 - - + + + + +
+
+
+ + +
Signal Buddy
+
+ +
+
+ +
+
+
+ Profile +

+

+

-
-
- -

-

-

-
-
- - - - -
+
+
+

+ 즐겨찾기 목록 +

+
+
+
+
+ +
+
+
+
+
diff --git a/src/main/resources/templates/admin/list.html b/src/main/resources/templates/admin/list.html index 0ee7f78b..c03be989 100644 --- a/src/main/resources/templates/admin/list.html +++ b/src/main/resources/templates/admin/list.html @@ -1,52 +1,68 @@ - + - - - 전체 사용자 조회 - - - - - - - + + + My Webcrumbs Plugin + + + - + +
+
+
+ + +
Signal Buddy
+
+ +
+
+ +
+
+ + + + + + + + + - - - + + + + + + -
-
프로필이름회원 지역즐겨찾기 개수
+ Avatar +
- - - - - - - - - -
-
- - - - - + + +
프로필이름회원 지역즐겨찾기 개수
- -
- - -
- + +
+
+
- + \ No newline at end of file diff --git a/src/main/resources/templates/admin/loginform.html b/src/main/resources/templates/admin/loginform.html index 351ede98..8958f0e3 100644 --- a/src/main/resources/templates/admin/loginform.html +++ b/src/main/resources/templates/admin/loginform.html @@ -1,14 +1,30 @@ - + + + Signal Buddy - 관리자 로그인 + -

관리자 로그인 페이지

-

-
- - - -
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/admin/main.html b/src/main/resources/templates/admin/main.html new file mode 100644 index 00000000..d9fbbcc4 --- /dev/null +++ b/src/main/resources/templates/admin/main.html @@ -0,0 +1,60 @@ + + + + + + My Webcrumbs Plugin + + + + +
+ +
+ +
+
+
+ + +
Signal Buddy
+
+
+
+

회원 정보 관리

+
+ + + +
+
+
+
+
+======= + Title + + +

main.html

+>>>>>>> Stashed changes + + \ No newline at end of file diff --git a/src/main/resources/templates/admin/withdrawal.html b/src/main/resources/templates/admin/withdrawal.html index 316d435b..1a9b1b09 100644 --- a/src/main/resources/templates/admin/withdrawal.html +++ b/src/main/resources/templates/admin/withdrawal.html @@ -1,62 +1,68 @@ - + - - - 탈퇴 사용자 조회 - - - - - - - - + + + 탈퇴 회원 관리 + + - - - + +
+
+
+ + +
Signal Buddy
+
+ +
+
+ +
+
+ + + + + + + + + - + + + + + + -
-
프로필이름요청 날짜삭제 요청
+ Avatar +
- - - - - - - - - -
- -
- - - - - + + +
프로필이름요청 날짜삭제 요청
- -
+
+
- - - - +
+
- + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/adminHeader.html b/src/main/resources/templates/fragments/adminHeader.html new file mode 100644 index 00000000..ee7d00b5 --- /dev/null +++ b/src/main/resources/templates/fragments/adminHeader.html @@ -0,0 +1,38 @@ + + + + + + + + +
+
+ + +
Signal Buddy
+
+ +
+
diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html index f6e59dca..e9034718 100644 --- a/src/main/resources/templates/fragments/header.html +++ b/src/main/resources/templates/fragments/header.html @@ -1,5 +1,6 @@ - + @@ -8,22 +9,32 @@
- +
Signal Buddy
diff --git a/src/main/resources/templates/fragments/sidebar.html b/src/main/resources/templates/fragments/sidebar.html index 687c5421..7ea63a22 100644 --- a/src/main/resources/templates/fragments/sidebar.html +++ b/src/main/resources/templates/fragments/sidebar.html @@ -6,18 +6,15 @@ diff --git a/src/main/resources/templates/main.html b/src/main/resources/templates/main.html new file mode 100644 index 00000000..77dc8da2 --- /dev/null +++ b/src/main/resources/templates/main.html @@ -0,0 +1,124 @@ + + + + + + SignalBuddy + + + + + + + + + + + + + +
+
+ +
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/member/bookmark/edit.html b/src/main/resources/templates/member/bookmark/edit.html index 9c295fcb..3316fd74 100644 --- a/src/main/resources/templates/member/bookmark/edit.html +++ b/src/main/resources/templates/member/bookmark/edit.html @@ -44,9 +44,8 @@

자주 가는 곳 - 수정<
- 취소 + th:href="@{/members/{memberId}/bookmarks(memberId=${user.memberId})}">취소
@@ -69,7 +68,7 @@

자주 가는 곳 - 수정< }, }) .then(response => { - window.location.href = '/members/1/bookmarks' + window.location.href = `/members/${id}/bookmarks` }) .catch(error => { console.error('삭제 실패:', error); diff --git a/src/main/resources/templates/member/bookmark/list.html b/src/main/resources/templates/member/bookmark/list.html index 90477ca6..7fb74eaa 100644 --- a/src/main/resources/templates/member/bookmark/list.html +++ b/src/main/resources/templates/member/bookmark/list.html @@ -17,7 +17,8 @@
-

자주가는 곳 (1/20)

+

자주가는 곳 (1/20)

@@ -30,13 +31,15 @@

+ edit

-
+
add_circle
@@ -47,11 +50,11 @@

- - - + +

diff --git a/src/main/resources/templates/member/edit.html b/src/main/resources/templates/member/edit.html index 0eb779d4..8b928690 100644 --- a/src/main/resources/templates/member/edit.html +++ b/src/main/resources/templates/member/edit.html @@ -20,29 +20,34 @@ class="font-title text-neutral-950 text-xl mb-8">계정 관리

Profile + th:src="${'/api/members/files/' + user.profileImageUrl}">
- - +
- - +
- - +
- - 취소 - + 취소 +
@@ -60,11 +65,11 @@ email, nickname } - if(password) { + if (password) { params.password = password } fetch(`/api/members/${id}`, { - method: "patch", + method: "PATCH", headers: { "Content-Type": "application/json", }, @@ -76,8 +81,7 @@ .then(data => { console.log('업데이트 성공:', data); alert("수정이 완료되었습니다.") - // TODO : Path Variable 수정 필요 - window.location.href = '/members/1' + window.location.href = `/members` }) .catch(error => { console.error('업데이트 실패:', error); diff --git a/src/main/resources/templates/member/feedback/list.html b/src/main/resources/templates/member/feedback/list.html index d7d2c17c..e94ec008 100644 --- a/src/main/resources/templates/member/feedback/list.html +++ b/src/main/resources/templates/member/feedback/list.html @@ -49,10 +49,10 @@

작성한 피드백

- - +
diff --git a/src/main/resources/templates/member/info.html b/src/main/resources/templates/member/info.html index ea126a98..67fe611c 100644 --- a/src/main/resources/templates/member/info.html +++ b/src/main/resources/templates/member/info.html @@ -20,27 +20,30 @@

계정 관리

Profile + th:src="${'/api/members/files/' + user.profileImageUrl}">
- - +
- - +
- 수정 + th:href="@{/members/edit}">수정
@@ -56,23 +59,30 @@

계정 관리

return; } fetch(`/api/members/${id}`, { - method: "delete", + method: "DELETE", headers: { "Content-Type": "application/json", } }) - .then(response => { - return response.json(); - }) .then(data => { console.log('회원 탈퇴 성공:', data); alert("탈퇴 성공했습니다.") - window.location.href = '/' + sendLogoutRequest(); }) .catch(error => { console.error('회원 탈퇴 실패:', error); }); } + const sendLogoutRequest = () => { + // HTML Form을 동적으로 생성해 POST 요청으로 로그아웃 처리 + const form = document.createElement("form"); + form.method = "POST"; + form.action = "/logout"; // Spring Security의 기본 로그아웃 경로 + + form.appendChild(csrfInput); + document.body.appendChild(form); + form.submit(); + } \ No newline at end of file diff --git a/src/main/resources/templates/member/loginform.html b/src/main/resources/templates/member/loginform.html index 6b70531f..8e12e0bf 100644 --- a/src/main/resources/templates/member/loginform.html +++ b/src/main/resources/templates/member/loginform.html @@ -1,14 +1,58 @@ - + + + Signal Buddy - 계정 관리 + + + -

로그인 페이지

-

-
- - - -
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/member/signup.html b/src/main/resources/templates/member/signup.html new file mode 100644 index 00000000..1522637c --- /dev/null +++ b/src/main/resources/templates/member/signup.html @@ -0,0 +1,61 @@ + + + + + + + + + Signal Buddy 회원가입 + + + + + + + + diff --git a/src/test/java/org/programmers/signalbuddy/domain/bookmark/service/BookmarkServiceTest.java b/src/test/java/org/programmers/signalbuddy/domain/bookmark/service/BookmarkServiceTest.java index 2824b0db..fc64c40d 100644 --- a/src/test/java/org/programmers/signalbuddy/domain/bookmark/service/BookmarkServiceTest.java +++ b/src/test/java/org/programmers/signalbuddy/domain/bookmark/service/BookmarkServiceTest.java @@ -13,13 +13,14 @@ import org.programmers.signalbuddy.domain.bookmark.dto.BookmarkResponse; import org.programmers.signalbuddy.domain.bookmark.entity.Bookmark; import org.programmers.signalbuddy.domain.bookmark.repository.BookmarkRepository; -import org.programmers.signalbuddy.domain.member.entity.enums.MemberRole; import org.programmers.signalbuddy.domain.member.entity.Member; +import org.programmers.signalbuddy.domain.member.entity.enums.MemberRole; import org.programmers.signalbuddy.domain.member.entity.enums.MemberStatus; import org.programmers.signalbuddy.domain.member.repository.MemberRepository; +import org.programmers.signalbuddy.global.dto.CustomUser2Member; +import org.programmers.signalbuddy.global.security.basic.CustomUserDetails; import org.programmers.signalbuddy.global.support.ServiceTest; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.security.SecurityProperties.User; import org.springframework.transaction.annotation.Transactional; @Transactional @@ -42,8 +43,8 @@ class BookmarkServiceTest extends ServiceTest { @BeforeEach void setup() { - member = Member.builder().email("bookmark@bookmark.com").password("123456").role(MemberRole.USER) - .nickname("bookmarkTest").memberStatus(MemberStatus.ACTIVITY) + member = Member.builder().email("bookmark@bookmark.com").password("123456") + .role(MemberRole.USER).nickname("bookmarkTest").memberStatus(MemberStatus.ACTIVITY) .profileImageUrl("https://book-test-image.com/test-123131").build(); member = memberRepository.save(member); @@ -57,8 +58,10 @@ void setup() { @Test @DisplayName("즐겨찾기 등록 테스트") void createBookmark() { - User user = new User(); - user.setName(member.getMemberId().toString()); + CustomUser2Member user = new CustomUser2Member( + new CustomUserDetails(member.getMemberId(), "", "", "", "", MemberRole.USER, + MemberStatus.ACTIVITY)); + final BookmarkRequest request = BookmarkRequest.builder().lat(37.12345).lng(127.12345) .address("test").build(); final BookmarkResponse response = bookmarkService.createBookmark(request, user); @@ -74,8 +77,10 @@ void createBookmark() { @Test @DisplayName("즐겨찾기 수정 테스트") void updateBookmark() { - User user = new User(); - user.setName(member.getMemberId().toString()); + CustomUser2Member user = new CustomUser2Member( + new CustomUserDetails(member.getMemberId(), "", "", "", "", MemberRole.USER, + MemberStatus.ACTIVITY)); + final BookmarkRequest request = BookmarkRequest.builder().lat(37.12345).lng(127.12345) .address("test").build(); @@ -94,8 +99,10 @@ void updateBookmark() { @Test @DisplayName("즐겨찾기 삭제 테스트") void deleteBookmark() { - User user = new User(); - user.setName(member.getMemberId().toString()); + CustomUser2Member user = new CustomUser2Member( + new CustomUserDetails(member.getMemberId(), "", "", "", "", MemberRole.USER, + MemberStatus.ACTIVITY)); + bookmarkService.deleteBookmark(bookmark.getBookmarkId(), user); final Optional found = bookmarkRepository.findById(bookmark.getBookmarkId()); diff --git a/src/test/java/org/programmers/signalbuddy/domain/comment/service/CommentServiceTest.java b/src/test/java/org/programmers/signalbuddy/domain/comment/service/CommentServiceTest.java index be7d3aa5..8bce2dee 100644 --- a/src/test/java/org/programmers/signalbuddy/domain/comment/service/CommentServiceTest.java +++ b/src/test/java/org/programmers/signalbuddy/domain/comment/service/CommentServiceTest.java @@ -25,7 +25,7 @@ import org.programmers.signalbuddy.domain.member.repository.MemberRepository; import org.programmers.signalbuddy.global.dto.CustomUser2Member; import org.programmers.signalbuddy.global.exception.BusinessException; -import org.programmers.signalbuddy.global.security.CustomUserDetails; +import org.programmers.signalbuddy.global.security.basic.CustomUserDetails; import org.programmers.signalbuddy.global.support.ServiceTest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; diff --git a/src/test/java/org/programmers/signalbuddy/domain/feedback/service/FeedbackServiceTest.java b/src/test/java/org/programmers/signalbuddy/domain/feedback/service/FeedbackServiceTest.java index 9a3f9ec1..44434886 100644 --- a/src/test/java/org/programmers/signalbuddy/domain/feedback/service/FeedbackServiceTest.java +++ b/src/test/java/org/programmers/signalbuddy/domain/feedback/service/FeedbackServiceTest.java @@ -19,7 +19,7 @@ import org.programmers.signalbuddy.domain.member.repository.MemberRepository; import org.programmers.signalbuddy.global.dto.CustomUser2Member; import org.programmers.signalbuddy.global.exception.BusinessException; -import org.programmers.signalbuddy.global.security.CustomUserDetails; +import org.programmers.signalbuddy.global.security.basic.CustomUserDetails; import org.programmers.signalbuddy.global.support.ServiceTest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; diff --git a/src/test/java/org/programmers/signalbuddy/domain/like/service/LikeServiceTest.java b/src/test/java/org/programmers/signalbuddy/domain/like/service/LikeServiceTest.java index fa80bf31..b83481ca 100644 --- a/src/test/java/org/programmers/signalbuddy/domain/like/service/LikeServiceTest.java +++ b/src/test/java/org/programmers/signalbuddy/domain/like/service/LikeServiceTest.java @@ -19,7 +19,7 @@ import org.programmers.signalbuddy.domain.member.entity.enums.MemberStatus; import org.programmers.signalbuddy.domain.member.repository.MemberRepository; import org.programmers.signalbuddy.global.dto.CustomUser2Member; -import org.programmers.signalbuddy.global.security.CustomUserDetails; +import org.programmers.signalbuddy.global.security.basic.CustomUserDetails; import org.programmers.signalbuddy.global.support.ServiceTest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; diff --git a/src/test/java/org/programmers/signalbuddy/domain/member/service/MemberServiceTest.java b/src/test/java/org/programmers/signalbuddy/domain/member/service/MemberServiceTest.java index 82c8777e..72753ae1 100644 --- a/src/test/java/org/programmers/signalbuddy/domain/member/service/MemberServiceTest.java +++ b/src/test/java/org/programmers/signalbuddy/domain/member/service/MemberServiceTest.java @@ -21,6 +21,7 @@ import org.programmers.signalbuddy.domain.member.dto.MemberResponse; import org.programmers.signalbuddy.domain.member.dto.MemberUpdateRequest; import org.programmers.signalbuddy.domain.member.repository.MemberRepository; +import org.springframework.mock.web.MockMultipartFile; @ExtendWith(MockitoExtension.class) class MemberServiceTest { @@ -89,18 +90,25 @@ void deleteMember() { @DisplayName("회원 가입 성공") void savedMember() { + MockMultipartFile profileImage = new MockMultipartFile( + "profileImage", + "", + "image/jpeg", + new byte[0] + ); + //given final MemberJoinRequest request = MemberJoinRequest.builder() .email("test2@example.com") .nickname("TestUser2") - .profileImageUrl("http://example.com/profile.jpg") .password("password123") + .profileImageUrl(profileImage) .build(); final Member expectedMember = Member.builder() .memberId(id) .email("test2@example.com") .nickname("TestUser2") - .profileImageUrl("http://example.com/profile.jpg") + .profileImageUrl("none") .memberStatus(MemberStatus.ACTIVITY) .role(MemberRole.USER).build(); diff --git a/submodule b/submodule index 2614115c..5dfa1f19 160000 --- a/submodule +++ b/submodule @@ -1 +1 @@ -Subproject commit 2614115c0562f96d2bb62180542f3094c542ca31 +Subproject commit 5dfa1f19fa19ad6a875af369d60348c920444b15