Merge pull request 'develop' (#5) from develop into main
All checks were successful
Build and Deploy API / build-and-deploy (push) Successful in 16s
All checks were successful
Build and Deploy API / build-and-deploy (push) Successful in 16s
Reviewed-on: #5
This commit is contained in:
커밋
57b11774eb
@ -14,7 +14,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
run: |
|
run: |
|
||||||
git clone --depth=1 --branch=${GITHUB_REF_NAME} \
|
git clone --depth=1 --branch=${GITHUB_REF_NAME} \
|
||||||
${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
http://gitea:3000/${GITHUB_REPOSITORY}.git .
|
||||||
|
|
||||||
- name: Configure Maven settings
|
- name: Configure Maven settings
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY target/gc-guide-api-*.jar app.jar
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@ -5,7 +5,9 @@ import com.gcsc.guide.dto.GoogleLoginRequest;
|
|||||||
import com.gcsc.guide.dto.UserResponse;
|
import com.gcsc.guide.dto.UserResponse;
|
||||||
import com.gcsc.guide.entity.User;
|
import com.gcsc.guide.entity.User;
|
||||||
import com.gcsc.guide.repository.UserRepository;
|
import com.gcsc.guide.repository.UserRepository;
|
||||||
|
import com.gcsc.guide.service.ActivityService;
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
|
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -26,12 +28,15 @@ public class AuthController {
|
|||||||
private final GoogleTokenVerifier googleTokenVerifier;
|
private final GoogleTokenVerifier googleTokenVerifier;
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final ActivityService activityService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Google ID Token으로 로그인/회원가입 처리 후 JWT 발급
|
* Google ID Token으로 로그인/회원가입 처리 후 JWT 발급
|
||||||
*/
|
*/
|
||||||
@PostMapping("/google")
|
@PostMapping("/google")
|
||||||
public ResponseEntity<AuthResponse> googleLogin(@Valid @RequestBody GoogleLoginRequest request) {
|
public ResponseEntity<AuthResponse> googleLogin(
|
||||||
|
@Valid @RequestBody GoogleLoginRequest request,
|
||||||
|
HttpServletRequest httpRequest) {
|
||||||
GoogleIdToken.Payload payload = googleTokenVerifier.verify(request.idToken());
|
GoogleIdToken.Payload payload = googleTokenVerifier.verify(request.idToken());
|
||||||
if (payload == null) {
|
if (payload == null) {
|
||||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "유효하지 않은 Google 토큰입니다");
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "유효하지 않은 Google 토큰입니다");
|
||||||
@ -54,6 +59,11 @@ public class AuthController {
|
|||||||
User userWithRoles = userRepository.findByEmailWithRoles(email)
|
User userWithRoles = userRepository.findByEmailWithRoles(email)
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
|
|
||||||
|
activityService.recordLogin(
|
||||||
|
userWithRoles.getId(),
|
||||||
|
httpRequest.getRemoteAddr(),
|
||||||
|
httpRequest.getHeader("User-Agent"));
|
||||||
|
|
||||||
String token = jwtTokenProvider.generateToken(
|
String token = jwtTokenProvider.generateToken(
|
||||||
userWithRoles.getId(), userWithRoles.getEmail(), userWithRoles.isAdmin());
|
userWithRoles.getId(), userWithRoles.getEmail(), userWithRoles.isAdmin());
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
package com.gcsc.guide.controller;
|
||||||
|
|
||||||
|
import com.gcsc.guide.dto.LoginHistoryResponse;
|
||||||
|
import com.gcsc.guide.dto.TrackPageViewRequest;
|
||||||
|
import com.gcsc.guide.service.ActivityService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 활동 기록 API
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/activity")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ActivityController {
|
||||||
|
|
||||||
|
private final ActivityService activityService;
|
||||||
|
|
||||||
|
/** 페이지 뷰 기록 */
|
||||||
|
@PostMapping("/track")
|
||||||
|
public ResponseEntity<Void> trackPageView(
|
||||||
|
Authentication authentication,
|
||||||
|
@Valid @RequestBody TrackPageViewRequest request) {
|
||||||
|
Long userId = (Long) authentication.getPrincipal();
|
||||||
|
activityService.trackPageView(userId, request.pagePath());
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 현재 사용자의 로그인 이력 조회 */
|
||||||
|
@GetMapping("/login-history")
|
||||||
|
public ResponseEntity<List<LoginHistoryResponse>> getLoginHistory(Authentication authentication) {
|
||||||
|
Long userId = (Long) authentication.getPrincipal();
|
||||||
|
return ResponseEntity.ok(activityService.getLoginHistory(userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
package com.gcsc.guide.controller;
|
||||||
|
|
||||||
|
import com.gcsc.guide.dto.AddPermissionRequest;
|
||||||
|
import com.gcsc.guide.dto.CreateRoleRequest;
|
||||||
|
import com.gcsc.guide.dto.RoleResponse;
|
||||||
|
import com.gcsc.guide.service.RoleService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자 롤/권한 관리 API
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/roles")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AdminRoleController {
|
||||||
|
|
||||||
|
private final RoleService roleService;
|
||||||
|
|
||||||
|
/** 전체 롤 목록 */
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<RoleResponse>> getRoles() {
|
||||||
|
return ResponseEntity.ok(roleService.getRoles());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 롤 생성 */
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<RoleResponse> createRole(@Valid @RequestBody CreateRoleRequest request) {
|
||||||
|
RoleResponse role = roleService.createRole(request.name(), request.description());
|
||||||
|
return ResponseEntity.created(URI.create("/api/admin/roles/" + role.id())).body(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 롤 수정 */
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<RoleResponse> updateRole(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@Valid @RequestBody CreateRoleRequest request) {
|
||||||
|
return ResponseEntity.ok(roleService.updateRole(id, request.name(), request.description()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 롤 삭제 */
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> deleteRole(@PathVariable Long id) {
|
||||||
|
roleService.deleteRole(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 롤의 URL 패턴 목록 */
|
||||||
|
@GetMapping("/{id}/permissions")
|
||||||
|
public ResponseEntity<List<String>> getPermissions(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(roleService.getPermissions(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** URL 패턴 추가 */
|
||||||
|
@PostMapping("/{id}/permissions")
|
||||||
|
public ResponseEntity<RoleResponse> addPermission(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@Valid @RequestBody AddPermissionRequest request) {
|
||||||
|
return ResponseEntity.ok(roleService.addPermission(id, request.urlPattern()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** URL 패턴 삭제 */
|
||||||
|
@DeleteMapping("/permissions/{permissionId}")
|
||||||
|
public ResponseEntity<Void> deletePermission(@PathVariable Long permissionId) {
|
||||||
|
roleService.deletePermission(permissionId);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package com.gcsc.guide.controller;
|
||||||
|
|
||||||
|
import com.gcsc.guide.dto.StatsResponse;
|
||||||
|
import com.gcsc.guide.service.UserService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자 통계 API
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/stats")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AdminStatsController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
/** 전체 통계 조회 */
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<StatsResponse> getStats() {
|
||||||
|
return ResponseEntity.ok(userService.getStats());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
package com.gcsc.guide.controller;
|
||||||
|
|
||||||
|
import com.gcsc.guide.dto.UpdateRolesRequest;
|
||||||
|
import com.gcsc.guide.dto.UserResponse;
|
||||||
|
import com.gcsc.guide.service.UserService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자 사용자 관리 API
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/users")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AdminUserController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
/** 전체 사용자 목록 조회 (status 필터 선택) */
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<UserResponse>> getUsers(
|
||||||
|
@RequestParam(required = false) String status) {
|
||||||
|
return ResponseEntity.ok(userService.getUsers(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 사용자 승인 (PENDING → ACTIVE) */
|
||||||
|
@PutMapping("/{id}/approve")
|
||||||
|
public ResponseEntity<UserResponse> approveUser(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(userService.approveUser(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 사용자 거절 (PENDING → REJECTED) */
|
||||||
|
@PutMapping("/{id}/reject")
|
||||||
|
public ResponseEntity<UserResponse> rejectUser(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(userService.rejectUser(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 사용자 비활성화 (ACTIVE → DISABLED) */
|
||||||
|
@PutMapping("/{id}/disable")
|
||||||
|
public ResponseEntity<UserResponse> disableUser(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(userService.disableUser(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 사용자 롤 업데이트 */
|
||||||
|
@PutMapping("/{id}/roles")
|
||||||
|
public ResponseEntity<UserResponse> updateUserRoles(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@Valid @RequestBody UpdateRolesRequest request) {
|
||||||
|
return ResponseEntity.ok(userService.updateUserRoles(id, request.roleIds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 관리자 권한 부여 */
|
||||||
|
@PostMapping("/{id}/admin")
|
||||||
|
public ResponseEntity<UserResponse> grantAdmin(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(userService.grantAdmin(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 관리자 권한 해제 */
|
||||||
|
@DeleteMapping("/{id}/admin")
|
||||||
|
public ResponseEntity<UserResponse> revokeAdmin(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(userService.revokeAdmin(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/main/java/com/gcsc/guide/controller/IssueController.java
Normal file
71
src/main/java/com/gcsc/guide/controller/IssueController.java
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package com.gcsc.guide.controller;
|
||||||
|
|
||||||
|
import com.gcsc.guide.dto.*;
|
||||||
|
import com.gcsc.guide.service.IssueService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이슈 관리 API
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/issues")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class IssueController {
|
||||||
|
|
||||||
|
private final IssueService issueService;
|
||||||
|
|
||||||
|
/** 이슈 목록 (status 필터, 페이징) */
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<Page<IssueResponse>> getIssues(
|
||||||
|
@RequestParam(required = false) String status,
|
||||||
|
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
|
||||||
|
return ResponseEntity.ok(issueService.getIssues(status, pageable));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 이슈 생성 */
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<IssueResponse> createIssue(
|
||||||
|
Authentication authentication,
|
||||||
|
@Valid @RequestBody CreateIssueRequest request) {
|
||||||
|
Long authorId = (Long) authentication.getPrincipal();
|
||||||
|
IssueResponse issue = issueService.createIssue(authorId, request);
|
||||||
|
return ResponseEntity.created(URI.create("/api/issues/" + issue.id())).body(issue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 이슈 상세 (코멘트 포함) */
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Map<String, Object>> getIssue(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(issueService.getIssueDetail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 이슈 수정 */
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<IssueResponse> updateIssue(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestBody UpdateIssueRequest request) {
|
||||||
|
return ResponseEntity.ok(issueService.updateIssue(id, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 코멘트 추가 */
|
||||||
|
@PostMapping("/{id}/comments")
|
||||||
|
public ResponseEntity<IssueCommentResponse> addComment(
|
||||||
|
@PathVariable Long id,
|
||||||
|
Authentication authentication,
|
||||||
|
@Valid @RequestBody CreateCommentRequest request) {
|
||||||
|
Long authorId = (Long) authentication.getPrincipal();
|
||||||
|
IssueCommentResponse comment = issueService.addComment(id, authorId, request.body());
|
||||||
|
return ResponseEntity.created(URI.create("/api/issues/" + id + "/comments/" + comment.id()))
|
||||||
|
.body(comment);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package com.gcsc.guide.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record AddPermissionRequest(
|
||||||
|
@NotBlank String urlPattern
|
||||||
|
) {
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package com.gcsc.guide.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record CreateCommentRequest(
|
||||||
|
@NotBlank String body
|
||||||
|
) {
|
||||||
|
}
|
||||||
14
src/main/java/com/gcsc/guide/dto/CreateIssueRequest.java
Normal file
14
src/main/java/com/gcsc/guide/dto/CreateIssueRequest.java
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package com.gcsc.guide.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record CreateIssueRequest(
|
||||||
|
@NotBlank String title,
|
||||||
|
String body,
|
||||||
|
String priority,
|
||||||
|
String project,
|
||||||
|
String location,
|
||||||
|
String giteaIssueUrl,
|
||||||
|
Integer giteaIssueId
|
||||||
|
) {
|
||||||
|
}
|
||||||
9
src/main/java/com/gcsc/guide/dto/CreateRoleRequest.java
Normal file
9
src/main/java/com/gcsc/guide/dto/CreateRoleRequest.java
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package com.gcsc.guide.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record CreateRoleRequest(
|
||||||
|
@NotBlank String name,
|
||||||
|
String description
|
||||||
|
) {
|
||||||
|
}
|
||||||
30
src/main/java/com/gcsc/guide/dto/IssueCommentResponse.java
Normal file
30
src/main/java/com/gcsc/guide/dto/IssueCommentResponse.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package com.gcsc.guide.dto;
|
||||||
|
|
||||||
|
import com.gcsc.guide.entity.IssueComment;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public record IssueCommentResponse(
|
||||||
|
Long id,
|
||||||
|
String body,
|
||||||
|
IssueResponse.AuthorInfo author,
|
||||||
|
LocalDateTime createdAt
|
||||||
|
) {
|
||||||
|
|
||||||
|
public static IssueCommentResponse from(IssueComment comment) {
|
||||||
|
IssueResponse.AuthorInfo authorInfo = comment.getAuthor() != null
|
||||||
|
? new IssueResponse.AuthorInfo(
|
||||||
|
comment.getAuthor().getId(),
|
||||||
|
comment.getAuthor().getName(),
|
||||||
|
comment.getAuthor().getEmail(),
|
||||||
|
comment.getAuthor().getAvatarUrl())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return new IssueCommentResponse(
|
||||||
|
comment.getId(),
|
||||||
|
comment.getBody(),
|
||||||
|
authorInfo,
|
||||||
|
comment.getCreatedAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/main/java/com/gcsc/guide/dto/IssueResponse.java
Normal file
53
src/main/java/com/gcsc/guide/dto/IssueResponse.java
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package com.gcsc.guide.dto;
|
||||||
|
|
||||||
|
import com.gcsc.guide.entity.Issue;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public record IssueResponse(
|
||||||
|
Long id,
|
||||||
|
String title,
|
||||||
|
String body,
|
||||||
|
String status,
|
||||||
|
String priority,
|
||||||
|
String project,
|
||||||
|
String location,
|
||||||
|
String giteaIssueUrl,
|
||||||
|
Integer giteaIssueId,
|
||||||
|
AuthorInfo author,
|
||||||
|
AuthorInfo assignee,
|
||||||
|
LocalDateTime createdAt,
|
||||||
|
LocalDateTime updatedAt
|
||||||
|
) {
|
||||||
|
|
||||||
|
public record AuthorInfo(Long id, String name, String email, String avatarUrl) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IssueResponse from(Issue issue) {
|
||||||
|
AuthorInfo authorInfo = issue.getAuthor() != null
|
||||||
|
? new AuthorInfo(issue.getAuthor().getId(), issue.getAuthor().getName(),
|
||||||
|
issue.getAuthor().getEmail(), issue.getAuthor().getAvatarUrl())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
AuthorInfo assigneeInfo = issue.getAssignee() != null
|
||||||
|
? new AuthorInfo(issue.getAssignee().getId(), issue.getAssignee().getName(),
|
||||||
|
issue.getAssignee().getEmail(), issue.getAssignee().getAvatarUrl())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return new IssueResponse(
|
||||||
|
issue.getId(),
|
||||||
|
issue.getTitle(),
|
||||||
|
issue.getBody(),
|
||||||
|
issue.getStatus(),
|
||||||
|
issue.getPriority(),
|
||||||
|
issue.getProject(),
|
||||||
|
issue.getLocation(),
|
||||||
|
issue.getGiteaIssueUrl(),
|
||||||
|
issue.getGiteaIssueId(),
|
||||||
|
authorInfo,
|
||||||
|
assigneeInfo,
|
||||||
|
issue.getCreatedAt(),
|
||||||
|
issue.getUpdatedAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/java/com/gcsc/guide/dto/LoginHistoryResponse.java
Normal file
22
src/main/java/com/gcsc/guide/dto/LoginHistoryResponse.java
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package com.gcsc.guide.dto;
|
||||||
|
|
||||||
|
import com.gcsc.guide.entity.LoginHistory;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public record LoginHistoryResponse(
|
||||||
|
Long id,
|
||||||
|
LocalDateTime loginAt,
|
||||||
|
String ipAddress,
|
||||||
|
String userAgent
|
||||||
|
) {
|
||||||
|
|
||||||
|
public static LoginHistoryResponse from(LoginHistory history) {
|
||||||
|
return new LoginHistoryResponse(
|
||||||
|
history.getId(),
|
||||||
|
history.getLoginAt(),
|
||||||
|
history.getIpAddress(),
|
||||||
|
history.getUserAgent()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/main/java/com/gcsc/guide/dto/StatsResponse.java
Normal file
12
src/main/java/com/gcsc/guide/dto/StatsResponse.java
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package com.gcsc.guide.dto;
|
||||||
|
|
||||||
|
public record StatsResponse(
|
||||||
|
long totalUsers,
|
||||||
|
long activeUsers,
|
||||||
|
long pendingUsers,
|
||||||
|
long rejectedUsers,
|
||||||
|
long disabledUsers,
|
||||||
|
long todayLogins,
|
||||||
|
long totalRoles
|
||||||
|
) {
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package com.gcsc.guide.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record TrackPageViewRequest(
|
||||||
|
@NotBlank String pagePath
|
||||||
|
) {
|
||||||
|
}
|
||||||
14
src/main/java/com/gcsc/guide/dto/UpdateIssueRequest.java
Normal file
14
src/main/java/com/gcsc/guide/dto/UpdateIssueRequest.java
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package com.gcsc.guide.dto;
|
||||||
|
|
||||||
|
public record UpdateIssueRequest(
|
||||||
|
String title,
|
||||||
|
String body,
|
||||||
|
String status,
|
||||||
|
String priority,
|
||||||
|
String project,
|
||||||
|
String location,
|
||||||
|
Long assigneeId,
|
||||||
|
String giteaIssueUrl,
|
||||||
|
Integer giteaIssueId
|
||||||
|
) {
|
||||||
|
}
|
||||||
10
src/main/java/com/gcsc/guide/dto/UpdateRolesRequest.java
Normal file
10
src/main/java/com/gcsc/guide/dto/UpdateRolesRequest.java
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package com.gcsc.guide.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record UpdateRolesRequest(
|
||||||
|
@NotNull List<Long> roleIds
|
||||||
|
) {
|
||||||
|
}
|
||||||
99
src/main/java/com/gcsc/guide/entity/Issue.java
Normal file
99
src/main/java/com/gcsc/guide/entity/Issue.java
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package com.gcsc.guide.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "issues")
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Issue {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 20)
|
||||||
|
private String status = "OPEN";
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 20)
|
||||||
|
private String priority = "NORMAL";
|
||||||
|
|
||||||
|
@Column(length = 100)
|
||||||
|
private String project;
|
||||||
|
|
||||||
|
@Column(length = 255)
|
||||||
|
private String location;
|
||||||
|
|
||||||
|
@Column(name = "gitea_issue_url", length = 500)
|
||||||
|
private String giteaIssueUrl;
|
||||||
|
|
||||||
|
@Column(name = "gitea_issue_id")
|
||||||
|
private Integer giteaIssueId;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "author_id")
|
||||||
|
private User author;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "assignee_id")
|
||||||
|
private User assignee;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
public Issue(String title, String body, String priority, String project,
|
||||||
|
String location, User author) {
|
||||||
|
this.title = title;
|
||||||
|
this.body = body;
|
||||||
|
this.priority = priority;
|
||||||
|
this.project = project;
|
||||||
|
this.location = location;
|
||||||
|
this.author = author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(String title, String body, String priority,
|
||||||
|
String project, String location) {
|
||||||
|
this.title = title;
|
||||||
|
this.body = body;
|
||||||
|
this.priority = priority;
|
||||||
|
this.project = project;
|
||||||
|
this.location = location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assignTo(User assignee) {
|
||||||
|
this.assignee = assignee;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void linkGiteaIssue(String giteaIssueUrl, Integer giteaIssueId) {
|
||||||
|
this.giteaIssueUrl = giteaIssueUrl;
|
||||||
|
this.giteaIssueId = giteaIssueId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/main/java/com/gcsc/guide/entity/IssueComment.java
Normal file
43
src/main/java/com/gcsc/guide/entity/IssueComment.java
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package com.gcsc.guide.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "issue_comments")
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class IssueComment {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "issue_id", nullable = false)
|
||||||
|
private Issue issue;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "author_id")
|
||||||
|
private User author;
|
||||||
|
|
||||||
|
@Column(nullable = false, columnDefinition = "TEXT")
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
public IssueComment(Issue issue, User author, String body) {
|
||||||
|
this.issue = issue;
|
||||||
|
this.author = author;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/main/java/com/gcsc/guide/entity/LoginHistory.java
Normal file
42
src/main/java/com/gcsc/guide/entity/LoginHistory.java
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package com.gcsc.guide.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "login_history")
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class LoginHistory {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "user_id")
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@Column(name = "login_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime loginAt;
|
||||||
|
|
||||||
|
@Column(name = "ip_address", length = 45)
|
||||||
|
private String ipAddress;
|
||||||
|
|
||||||
|
@Column(name = "user_agent", length = 500)
|
||||||
|
private String userAgent;
|
||||||
|
|
||||||
|
public LoginHistory(User user, String ipAddress, String userAgent) {
|
||||||
|
this.user = user;
|
||||||
|
this.ipAddress = ipAddress;
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.loginAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/java/com/gcsc/guide/entity/PageView.java
Normal file
38
src/main/java/com/gcsc/guide/entity/PageView.java
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package com.gcsc.guide.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "page_views")
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class PageView {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "user_id")
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@Column(name = "page_path", nullable = false)
|
||||||
|
private String pagePath;
|
||||||
|
|
||||||
|
@Column(name = "viewed_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime viewedAt;
|
||||||
|
|
||||||
|
public PageView(User user, String pagePath) {
|
||||||
|
this.user = user;
|
||||||
|
this.pagePath = pagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
this.viewedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package com.gcsc.guide.exception;
|
||||||
|
|
||||||
|
public class BusinessException extends RuntimeException {
|
||||||
|
|
||||||
|
public BusinessException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package com.gcsc.guide.exception;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(ResourceNotFoundException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleNotFound(ResourceNotFoundException e) {
|
||||||
|
return buildResponse(HttpStatus.NOT_FOUND, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(BusinessException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleBusiness(BusinessException e) {
|
||||||
|
return buildResponse(HttpStatus.BAD_REQUEST, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException e) {
|
||||||
|
String message = e.getBindingResult().getFieldErrors().stream()
|
||||||
|
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
|
||||||
|
.reduce((a, b) -> a + ", " + b)
|
||||||
|
.orElse("입력값이 올바르지 않습니다");
|
||||||
|
return buildResponse(HttpStatus.BAD_REQUEST, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(ResponseStatusException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleResponseStatus(ResponseStatusException e) {
|
||||||
|
return buildResponse(HttpStatus.valueOf(e.getStatusCode().value()), e.getReason());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleUnexpected(Exception e) {
|
||||||
|
log.error("예상치 못한 오류 발생", e);
|
||||||
|
return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다");
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<Map<String, Object>> buildResponse(HttpStatus status, String message) {
|
||||||
|
return ResponseEntity.status(status).body(Map.of(
|
||||||
|
"status", status.value(),
|
||||||
|
"error", status.getReasonPhrase(),
|
||||||
|
"message", message != null ? message : "알 수 없는 오류",
|
||||||
|
"timestamp", LocalDateTime.now().toString()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.gcsc.guide.exception;
|
||||||
|
|
||||||
|
public class ResourceNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
public ResourceNotFoundException(String resource, Long id) {
|
||||||
|
super(resource + "을(를) 찾을 수 없습니다 (id=" + id + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.gcsc.guide.repository;
|
||||||
|
|
||||||
|
import com.gcsc.guide.entity.IssueComment;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface IssueCommentRepository extends JpaRepository<IssueComment, Long> {
|
||||||
|
|
||||||
|
@Query("SELECT c FROM IssueComment c JOIN FETCH c.author WHERE c.issue.id = :issueId ORDER BY c.createdAt ASC")
|
||||||
|
List<IssueComment> findByIssueIdWithAuthor(Long issueId);
|
||||||
|
}
|
||||||
17
src/main/java/com/gcsc/guide/repository/IssueRepository.java
Normal file
17
src/main/java/com/gcsc/guide/repository/IssueRepository.java
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package com.gcsc.guide.repository;
|
||||||
|
|
||||||
|
import com.gcsc.guide.entity.Issue;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface IssueRepository extends JpaRepository<Issue, Long> {
|
||||||
|
|
||||||
|
Page<Issue> findByStatus(String status, Pageable pageable);
|
||||||
|
|
||||||
|
@Query("SELECT i FROM Issue i LEFT JOIN FETCH i.author LEFT JOIN FETCH i.assignee WHERE i.id = :id")
|
||||||
|
Optional<Issue> findByIdWithUsers(Long id);
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package com.gcsc.guide.repository;
|
||||||
|
|
||||||
|
import com.gcsc.guide.entity.LoginHistory;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface LoginHistoryRepository extends JpaRepository<LoginHistory, Long> {
|
||||||
|
|
||||||
|
List<LoginHistory> findByUserIdOrderByLoginAtDesc(Long userId);
|
||||||
|
|
||||||
|
long countByLoginAtAfter(LocalDateTime after);
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.gcsc.guide.repository;
|
||||||
|
|
||||||
|
import com.gcsc.guide.entity.PageView;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface PageViewRepository extends JpaRepository<PageView, Long> {
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.gcsc.guide.repository;
|
||||||
|
|
||||||
|
import com.gcsc.guide.entity.RoleUrlPattern;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface RoleUrlPatternRepository extends JpaRepository<RoleUrlPattern, Long> {
|
||||||
|
}
|
||||||
45
src/main/java/com/gcsc/guide/service/ActivityService.java
Normal file
45
src/main/java/com/gcsc/guide/service/ActivityService.java
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package com.gcsc.guide.service;
|
||||||
|
|
||||||
|
import com.gcsc.guide.dto.LoginHistoryResponse;
|
||||||
|
import com.gcsc.guide.entity.LoginHistory;
|
||||||
|
import com.gcsc.guide.entity.PageView;
|
||||||
|
import com.gcsc.guide.entity.User;
|
||||||
|
import com.gcsc.guide.exception.ResourceNotFoundException;
|
||||||
|
import com.gcsc.guide.repository.LoginHistoryRepository;
|
||||||
|
import com.gcsc.guide.repository.PageViewRepository;
|
||||||
|
import com.gcsc.guide.repository.UserRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ActivityService {
|
||||||
|
|
||||||
|
private final LoginHistoryRepository loginHistoryRepository;
|
||||||
|
private final PageViewRepository pageViewRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void recordLogin(Long userId, String ipAddress, String userAgent) {
|
||||||
|
User user = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("사용자", userId));
|
||||||
|
loginHistoryRepository.save(new LoginHistory(user, ipAddress, userAgent));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void trackPageView(Long userId, String pagePath) {
|
||||||
|
User user = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("사용자", userId));
|
||||||
|
pageViewRepository.save(new PageView(user, pagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<LoginHistoryResponse> getLoginHistory(Long userId) {
|
||||||
|
return loginHistoryRepository.findByUserIdOrderByLoginAtDesc(userId).stream()
|
||||||
|
.map(LoginHistoryResponse::from)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
118
src/main/java/com/gcsc/guide/service/IssueService.java
Normal file
118
src/main/java/com/gcsc/guide/service/IssueService.java
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package com.gcsc.guide.service;
|
||||||
|
|
||||||
|
import com.gcsc.guide.dto.*;
|
||||||
|
import com.gcsc.guide.entity.Issue;
|
||||||
|
import com.gcsc.guide.entity.IssueComment;
|
||||||
|
import com.gcsc.guide.entity.User;
|
||||||
|
import com.gcsc.guide.exception.ResourceNotFoundException;
|
||||||
|
import com.gcsc.guide.repository.IssueCommentRepository;
|
||||||
|
import com.gcsc.guide.repository.IssueRepository;
|
||||||
|
import com.gcsc.guide.repository.UserRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class IssueService {
|
||||||
|
|
||||||
|
private final IssueRepository issueRepository;
|
||||||
|
private final IssueCommentRepository issueCommentRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Page<IssueResponse> getIssues(String status, Pageable pageable) {
|
||||||
|
Page<Issue> issues;
|
||||||
|
if (status != null && !status.isBlank()) {
|
||||||
|
issues = issueRepository.findByStatus(status.toUpperCase(), pageable);
|
||||||
|
} else {
|
||||||
|
issues = issueRepository.findAll(pageable);
|
||||||
|
}
|
||||||
|
return issues.map(IssueResponse::from);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public IssueResponse createIssue(Long authorId, CreateIssueRequest request) {
|
||||||
|
User author = userRepository.findById(authorId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("사용자", authorId));
|
||||||
|
|
||||||
|
Issue issue = new Issue(
|
||||||
|
request.title(),
|
||||||
|
request.body(),
|
||||||
|
request.priority() != null ? request.priority() : "NORMAL",
|
||||||
|
request.project(),
|
||||||
|
request.location(),
|
||||||
|
author
|
||||||
|
);
|
||||||
|
|
||||||
|
if (request.giteaIssueUrl() != null) {
|
||||||
|
issue.linkGiteaIssue(request.giteaIssueUrl(), request.giteaIssueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return IssueResponse.from(issueRepository.save(issue));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Map<String, Object> getIssueDetail(Long issueId) {
|
||||||
|
Issue issue = issueRepository.findByIdWithUsers(issueId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("이슈", issueId));
|
||||||
|
|
||||||
|
List<IssueCommentResponse> comments = issueCommentRepository
|
||||||
|
.findByIssueIdWithAuthor(issueId).stream()
|
||||||
|
.map(IssueCommentResponse::from)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Map.of(
|
||||||
|
"issue", IssueResponse.from(issue),
|
||||||
|
"comments", comments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public IssueResponse updateIssue(Long issueId, UpdateIssueRequest request) {
|
||||||
|
Issue issue = issueRepository.findByIdWithUsers(issueId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("이슈", issueId));
|
||||||
|
|
||||||
|
if (request.title() != null) {
|
||||||
|
issue.update(
|
||||||
|
request.title(),
|
||||||
|
request.body(),
|
||||||
|
request.priority() != null ? request.priority() : issue.getPriority(),
|
||||||
|
request.project() != null ? request.project() : issue.getProject(),
|
||||||
|
request.location() != null ? request.location() : issue.getLocation()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.status() != null) {
|
||||||
|
issue.updateStatus(request.status().toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.assigneeId() != null) {
|
||||||
|
User assignee = userRepository.findById(request.assigneeId())
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("담당자", request.assigneeId()));
|
||||||
|
issue.assignTo(assignee);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.giteaIssueUrl() != null) {
|
||||||
|
issue.linkGiteaIssue(request.giteaIssueUrl(), request.giteaIssueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return IssueResponse.from(issueRepository.save(issue));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public IssueCommentResponse addComment(Long issueId, Long authorId, String body) {
|
||||||
|
Issue issue = issueRepository.findById(issueId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("이슈", issueId));
|
||||||
|
User author = userRepository.findById(authorId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("사용자", authorId));
|
||||||
|
|
||||||
|
IssueComment comment = new IssueComment(issue, author, body);
|
||||||
|
return IssueCommentResponse.from(issueCommentRepository.save(comment));
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/main/java/com/gcsc/guide/service/RoleService.java
Normal file
88
src/main/java/com/gcsc/guide/service/RoleService.java
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package com.gcsc.guide.service;
|
||||||
|
|
||||||
|
import com.gcsc.guide.dto.RoleResponse;
|
||||||
|
import com.gcsc.guide.entity.Role;
|
||||||
|
import com.gcsc.guide.entity.RoleUrlPattern;
|
||||||
|
import com.gcsc.guide.exception.BusinessException;
|
||||||
|
import com.gcsc.guide.exception.ResourceNotFoundException;
|
||||||
|
import com.gcsc.guide.repository.RoleRepository;
|
||||||
|
import com.gcsc.guide.repository.RoleUrlPatternRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RoleService {
|
||||||
|
|
||||||
|
private final RoleRepository roleRepository;
|
||||||
|
private final RoleUrlPatternRepository roleUrlPatternRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<RoleResponse> getRoles() {
|
||||||
|
return roleRepository.findAllWithUrlPatterns().stream()
|
||||||
|
.map(RoleResponse::from)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public RoleResponse createRole(String name, String description) {
|
||||||
|
roleRepository.findByName(name).ifPresent(r -> {
|
||||||
|
throw new BusinessException("이미 존재하는 롤 이름입니다: " + name);
|
||||||
|
});
|
||||||
|
Role role = new Role(name, description);
|
||||||
|
return RoleResponse.from(roleRepository.save(role));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public RoleResponse updateRole(Long roleId, String name, String description) {
|
||||||
|
Role role = findRoleById(roleId);
|
||||||
|
roleRepository.findByName(name)
|
||||||
|
.filter(r -> !r.getId().equals(roleId))
|
||||||
|
.ifPresent(r -> {
|
||||||
|
throw new BusinessException("이미 존재하는 롤 이름입니다: " + name);
|
||||||
|
});
|
||||||
|
role.update(name, description);
|
||||||
|
return RoleResponse.from(roleRepository.save(role));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteRole(Long roleId) {
|
||||||
|
if (!roleRepository.existsById(roleId)) {
|
||||||
|
throw new ResourceNotFoundException("롤", roleId);
|
||||||
|
}
|
||||||
|
roleRepository.deleteById(roleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<String> getPermissions(Long roleId) {
|
||||||
|
Role role = roleRepository.findByIdWithUrlPatterns(roleId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("롤", roleId));
|
||||||
|
return role.getUrlPatterns().stream()
|
||||||
|
.map(RoleUrlPattern::getUrlPattern)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public RoleResponse addPermission(Long roleId, String urlPattern) {
|
||||||
|
Role role = roleRepository.findByIdWithUrlPatterns(roleId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("롤", roleId));
|
||||||
|
role.getUrlPatterns().add(new RoleUrlPattern(role, urlPattern));
|
||||||
|
return RoleResponse.from(roleRepository.save(role));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deletePermission(Long permissionId) {
|
||||||
|
if (!roleUrlPatternRepository.existsById(permissionId)) {
|
||||||
|
throw new ResourceNotFoundException("권한", permissionId);
|
||||||
|
}
|
||||||
|
roleUrlPatternRepository.deleteById(permissionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Role findRoleById(Long roleId) {
|
||||||
|
return roleRepository.findById(roleId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("롤", roleId));
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/main/java/com/gcsc/guide/service/UserService.java
Normal file
120
src/main/java/com/gcsc/guide/service/UserService.java
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package com.gcsc.guide.service;
|
||||||
|
|
||||||
|
import com.gcsc.guide.dto.StatsResponse;
|
||||||
|
import com.gcsc.guide.dto.UserResponse;
|
||||||
|
import com.gcsc.guide.entity.Role;
|
||||||
|
import com.gcsc.guide.entity.User;
|
||||||
|
import com.gcsc.guide.entity.UserStatus;
|
||||||
|
import com.gcsc.guide.exception.BusinessException;
|
||||||
|
import com.gcsc.guide.exception.ResourceNotFoundException;
|
||||||
|
import com.gcsc.guide.repository.LoginHistoryRepository;
|
||||||
|
import com.gcsc.guide.repository.RoleRepository;
|
||||||
|
import com.gcsc.guide.repository.UserRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserService {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final RoleRepository roleRepository;
|
||||||
|
private final LoginHistoryRepository loginHistoryRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<UserResponse> getUsers(String status) {
|
||||||
|
List<User> users;
|
||||||
|
if (status != null && !status.isBlank()) {
|
||||||
|
UserStatus userStatus = UserStatus.valueOf(status.toUpperCase());
|
||||||
|
users = userRepository.findByStatus(userStatus);
|
||||||
|
} else {
|
||||||
|
users = userRepository.findAll();
|
||||||
|
}
|
||||||
|
return users.stream().map(UserResponse::from).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UserResponse approveUser(Long userId) {
|
||||||
|
User user = findUserById(userId);
|
||||||
|
if (user.getStatus() != UserStatus.PENDING) {
|
||||||
|
throw new BusinessException("PENDING 상태의 사용자만 승인할 수 있습니다 (현재: " + user.getStatus() + ")");
|
||||||
|
}
|
||||||
|
user.activate();
|
||||||
|
return UserResponse.from(userRepository.save(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UserResponse rejectUser(Long userId) {
|
||||||
|
User user = findUserById(userId);
|
||||||
|
if (user.getStatus() != UserStatus.PENDING) {
|
||||||
|
throw new BusinessException("PENDING 상태의 사용자만 거절할 수 있습니다 (현재: " + user.getStatus() + ")");
|
||||||
|
}
|
||||||
|
user.reject();
|
||||||
|
return UserResponse.from(userRepository.save(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UserResponse disableUser(Long userId) {
|
||||||
|
User user = findUserById(userId);
|
||||||
|
if (user.getStatus() != UserStatus.ACTIVE) {
|
||||||
|
throw new BusinessException("ACTIVE 상태의 사용자만 비활성화할 수 있습니다 (현재: " + user.getStatus() + ")");
|
||||||
|
}
|
||||||
|
user.disable();
|
||||||
|
return UserResponse.from(userRepository.save(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UserResponse updateUserRoles(Long userId, List<Long> roleIds) {
|
||||||
|
User user = findUserById(userId);
|
||||||
|
Set<Role> roles = new HashSet<>(roleRepository.findAllById(roleIds));
|
||||||
|
if (roles.size() != roleIds.size()) {
|
||||||
|
throw new BusinessException("일부 롤을 찾을 수 없습니다");
|
||||||
|
}
|
||||||
|
user.updateRoles(roles);
|
||||||
|
userRepository.save(user);
|
||||||
|
|
||||||
|
return UserResponse.from(userRepository.findByIdWithRoles(userId).orElseThrow());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UserResponse grantAdmin(Long userId) {
|
||||||
|
User user = findUserById(userId);
|
||||||
|
user.grantAdmin();
|
||||||
|
return UserResponse.from(userRepository.save(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UserResponse revokeAdmin(Long userId) {
|
||||||
|
User user = findUserById(userId);
|
||||||
|
user.revokeAdmin();
|
||||||
|
return UserResponse.from(userRepository.save(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public StatsResponse getStats() {
|
||||||
|
long totalUsers = userRepository.count();
|
||||||
|
long activeUsers = userRepository.countByStatus(UserStatus.ACTIVE);
|
||||||
|
long pendingUsers = userRepository.countByStatus(UserStatus.PENDING);
|
||||||
|
long rejectedUsers = userRepository.countByStatus(UserStatus.REJECTED);
|
||||||
|
long disabledUsers = userRepository.countByStatus(UserStatus.DISABLED);
|
||||||
|
long todayLogins = loginHistoryRepository.countByLoginAtAfter(
|
||||||
|
LocalDate.now().atStartOfDay());
|
||||||
|
long totalRoles = roleRepository.count();
|
||||||
|
|
||||||
|
return new StatsResponse(
|
||||||
|
totalUsers, activeUsers, pendingUsers,
|
||||||
|
rejectedUsers, disabledUsers, todayLogins, totalRoles
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private User findUserById(Long userId) {
|
||||||
|
return userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("사용자", userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
불러오는 중...
Reference in New Issue
Block a user