Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
786721a
[BE] [FEAT] Excel 처리용 Apache POI 의존성 추가
Yooonjeong Feb 13, 2026
d41a14a
[BE] [FEAT] 신입부원/준회원/정회원/선배 구분용 enum 추가
Yooonjeong Feb 13, 2026
22a5bae
[BE] [FEAT] 기존의 isNewMember 필드 grade 필드로 변경
Yooonjeong Feb 13, 2026
62f07e5
[BE] [FEAT] Gender, Role enum에 from 메서드 추가
Yooonjeong Feb 13, 2026
c562ec7
[BE] [FEAT] 엑셀 데이터 적용용 User 내부 메서드 정의
Yooonjeong Feb 13, 2026
7a08724
[BE] [FEAT] 신규 회원가입 시 PENDING_MEMBER role 및 NEW_MEMBER grade 설정
Yooonjeong Feb 13, 2026
68dec64
[BE] [FEAT] 관리자 페이지 사용자 조회용 Request DTO 정의
Yooonjeong Feb 13, 2026
6497b56
[BE] [FEAT] 관리자 페이지 사용자 조회용 Response DTO 정의
Yooonjeong Feb 13, 2026
c1b8e18
[BE] [FEAT] 엑셀 파일의 한 행을 정의하는 DTO 추가
Yooonjeong Feb 13, 2026
df06ede
[BE] [FEAT] 엑셀 동기화 결과 반환 DTO 정의
Yooonjeong Feb 13, 2026
063ae3c
[BE] [FEAT] 엑셀 관련 ErrorCode 정의
Yooonjeong Feb 13, 2026
96c4be0
[BE] [REFACTOR] 로그인 시 비밀번호 검증 없이 일치 여부만 체크하도록 변경
Yooonjeong Feb 13, 2026
17932cb
[BE] [FEAT] UserStatus 기반 조회 쿼리 정의
Yooonjeong Feb 13, 2026
b86ab35
[BE] [FEAT] 관리자 조회 및 프로젝션용 Repository 정의
Yooonjeong Feb 13, 2026
8458f8f
[BE] [FEAT] 엑셀 추출 사용자 데이터 DB 동기화 로직 구현
Yooonjeong Feb 13, 2026
b0abfce
[BE] [FEAT] 관리자 페이지 사용자 관리 service 구현
Yooonjeong Feb 13, 2026
6e32925
[BE] [FEAT] 관리자 페이지 사용자 관리 controller 구현
Yooonjeong Feb 13, 2026
32ea2f5
[BE] [FEAT] 관리자 페이지용 선배 등급 변경 API 추가
Yooonjeong Feb 13, 2026
4342654
Merge branch 'main' into 20260210_#206_관리자_회원_관리_기능_추가
Yooonjeong Feb 13, 2026
83e3cc3
[BE] [REMOVE] 불필요한 정적 팩토리 메서드 제거
Yooonjeong Feb 13, 2026
d152530
[BE] [FEAT] grade 기본값 설정 추가
Yooonjeong Feb 13, 2026
3dd1215
[BE] [FEAT] Apache POI 최신 버전(5.5.1)으로 업데이트
Yooonjeong Feb 13, 2026
e88e4a2
[BE] [FEAT] Account에 owner_id와 type 유니크 제약 추가
Yooonjeong Feb 13, 2026
d0ccbac
[BE] [REFACTOR] @Valid를 통해 검증되는 String 중복 검증 제거
Yooonjeong Feb 13, 2026
a9766ec
[BE] [REFACTOR] 불필요 매핑 메서드 제거
Yooonjeong Feb 13, 2026
ffe13c0
[BE] [FEAT] 엑셀 파일 동기화 로깅 추가
Yooonjeong Feb 13, 2026
fbff493
[BE] [REFACTOR] 상태 변경 메서드 UserService로 이동 및 AdminUserService에 적용
Yooonjeong Feb 13, 2026
5ea09f0
[BE] [FEAT] User 엔티티에 로그인 가능 여부 판단 메서드 추가
Yooonjeong Feb 13, 2026
b86b71d
[BE] [REFACTOR] UserStatus의 PENDING 삭제
Yooonjeong Feb 13, 2026
961060c
[BE] [FEAT] 활동 상태가 아닌 경우 에러 코드 정의
Yooonjeong Feb 13, 2026
32016b4
[BE] [FEAT] 로그인 시 탈퇴 회원 차단 로직 추가
Yooonjeong Feb 13, 2026
346fc77
[BE] [FEAT] 관리자용 사용자 조회 시 기본 정렬 추가 (기수 최신순, 이름 오름차순)
Yooonjeong Feb 13, 2026
a093b5b
[BE] [DOCS] 관리자 API Swagger Docs 수정
Yooonjeong Feb 13, 2026
9c87ad2
[BE] [FIX] 엑셀 데이터 동기화 중 비활성화 시 SYSTEM_ADMIN 제외
Yooonjeong Feb 13, 2026
b99793a
[BE] [REFACTOR] 탈퇴 회원 로그인 차단 에러 코드 변경
Yooonjeong Feb 13, 2026
b06b212
[BE] [REFACTOR] 엑셀 파싱 시 DataFormatter를 로컬 변수로 변경하여 스레드 안전성 개선
Yooonjeong Feb 13, 2026
c48fcb6
[BE] [DOCS] 회원 탈퇴 Swagger Docs 수정
Yooonjeong Feb 13, 2026
0bf7f35
[BE] [REFACTOR] 확장자 검증 시 소문자 변환 추가
Yooonjeong Feb 13, 2026
54a564c
[BE] [REFACTOR] MANAGER role 검증 삭제
Yooonjeong Feb 14, 2026
1c2e9e6
[BE] [FEAT] 관리자 회원 조회 시 전화번호 검색 기능 추가
Yooonjeong Feb 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ dependencies {

// Thymleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// Apache POI (Excel)
implementation 'org.apache.poi:poi-ooxml:5.5.1'
}

jacoco {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.sejongisc.backend.admin.dto.AdminUserRequest;
import org.sejongisc.backend.user.dto.UserInfoResponse; // 기존 DTO 활용
import org.sejongisc.backend.admin.dto.AdminUserResponse;
import org.sejongisc.backend.admin.dto.ExcelSyncResponse;
import org.sejongisc.backend.admin.service.AdminUserService;
import org.sejongisc.backend.user.entity.Role;
import org.sejongisc.backend.user.entity.UserStatus;
import org.sejongisc.backend.user.service.UserService;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.Map;
import java.util.UUID;

@RestController
Expand All @@ -24,55 +24,77 @@
@Tag(name = "관리자 API", description = "운영진 및 개발자용 회원 관리 API")
public class AdminUserController {

private final UserService userService;
private final AdminUserService adminUserService;

// --- [회장/운영진용] 회원 관리 API ---

@Operation(summary = "엑셀 명단 업로드 및 동기화", description = "엑셀 파일을 업로드하여 신규 회원을 등록하고, 기존 회원의 기수/직위를 갱신합니다.")
@Operation(
summary = "엑셀 명단 업로드 및 동기화",
description = """
엑셀 파일을 업로드하여 신규 회원을 등록하고, 기존 회원의 기수/직위를 갱신합니다. (회장/관리자용)
- .xlsx 형식만 업로드 가능합니다. \s
- 학번 기준으로 회원을 생성하거나 기존 정보를 갱신합니다. \s
- 신규 회원의 초기 비밀번호는 전화번호 숫자만(예: 01012345678)으로 설정됩니다.
"""
)
@PostMapping(value = "/upload-excel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')") // 회장, 개발자만 가능
public ResponseEntity<?> uploadMemberExcel(@RequestPart("file") MultipartFile file) {
// 엑셀 파싱 및 DB 동기화 결과 반환
//ExcelSyncResultDto result = adminUserService.syncMembersFromExcel(file);
//return ResponseEntity.ok(result);
return null;
public ResponseEntity<ExcelSyncResponse> uploadMemberExcel(@RequestPart("file") MultipartFile file) {
return ResponseEntity.ok(adminUserService.syncUsersFromExcel(file));
}

@Operation(summary = "전체 회원 목록 조회", description = "모든 회원의 정보를 조회합니다. (회장/관리자용)")
@GetMapping("")
@PreAuthorize("hasAnyRole('SYSTEM_ADMIN', 'MANAGER')")
public ResponseEntity<List<UserInfoResponse>> getAllUsers(@RequestBody AdminUserRequest request) {
//return ResponseEntity.ok(userService.findAllUsers()); // TODO : 전체 조회, 기수별 조회, 이름 검색 등 기능 추가 (페이징은 추후 고려)
return null;
@Operation(
summary = "전체 회원 목록 조회",
description = """
모든 회원의 정보를 조회합니다. (시스템 관리자용)
- 키워드, 기수, 권한(Role), 활동 상태(UserStatus) 조건으로 필터 조회합니다. \s
- 키워드는 이름, 학번, 이메일 기준으로 검색됩니다. \s
- 조건 미입력 시 전체 조회됩니다. \s
- 정렬 기준: 기수 최신순 → 이름 오름차순
"""
)
@GetMapping
@PreAuthorize("hasAnyRole('SYSTEM_ADMIN')")
public ResponseEntity<List<AdminUserResponse>> getAllUsers(@ModelAttribute AdminUserRequest request) {
// TODO: 페이징 추후 고려
return ResponseEntity.ok(adminUserService.findAllUsers(request));
}

@Operation(summary = "회원 활동 상태 변경", description = "ACTIVE, INACTIVE, GRADUATED 등으로 상태를 변경합니다.")
@Operation(summary = "회원 활동 상태 변경", description = "ACTIVE, INACTIVE, GRADUATED 등으로 상태를 변경합니다. (시스템 관리자용)")
@PatchMapping("/{userId}/status")
@PreAuthorize("hasAnyRole('SYSTEM_ADMIN', 'MANAGER')")
@PreAuthorize("hasAnyRole('SYSTEM_ADMIN')")
public ResponseEntity<?> updateUserStatus(
@PathVariable UUID userId,
@RequestParam UserStatus status) {
//userService.updateUserStatus(userId, status);
return ResponseEntity.ok(Map.of("message", "사용자 상태가 " + status + "(으)로 변경되었습니다."));
adminUserService.updateUserStatus(userId, status);
return ResponseEntity.noContent().build();
}

// --- [시스템 관리자용 or 회장용] 권한 및 계정 제어 API ---
// TODO : 회장 권한 논의 필요
@Operation(summary = "회원 권한 변경", description = "특정 유저의 Role(PRESIDENT, VICE_PRESIDENT, TEAM_LEADER)을 변경합니다.)")
@Operation(summary = "회원 권한 변경", description = "특정 유저의 Role(PRESIDENT, VICE_PRESIDENT, TEAM_LEADER)을 변경합니다. (시스템 관리자용)")
@PatchMapping("/{userId}/role")
@PreAuthorize("hasRole('SYSTEM_ADMIN')")
public ResponseEntity<?> updateUserRole(
@PathVariable UUID userId,
@RequestParam Role role) {
//userService.updateUserRole(userId, role);
return ResponseEntity.ok(Map.of("message", "사용자 권한이 " + role + "(으)로 변경되었습니다."));
adminUserService.updateUserRole(userId, role);
return ResponseEntity.noContent().build();
}

@Operation(summary = "회원 강제 탈퇴", description = "시스템에서 유저를 완전히 삭제합니다. (시스템 관리자용)")
@Operation(summary = "선배(SENIOR) 등급 변경", description = "특정 유저를 선배(SENIOR) 등급으로 변경합니다. (회장/관리자용)")
@PatchMapping("/{userId}/senior")
@PreAuthorize("hasAnyRole('PRESIDENT', 'SYSTEM_ADMIN')")
public ResponseEntity<Void> promoteToSenior(@PathVariable UUID userId) {
adminUserService.promoteToSenior(userId);
return ResponseEntity.noContent().build();
}

@Operation(summary = "회원 강제 탈퇴", description = "시스템에서 유저를 탈퇴 처리합니다. (시스템 관리자용)")
@DeleteMapping("/{userId}")
@PreAuthorize("hasRole('SYSTEM_ADMIN')")
public ResponseEntity<?> forceDeleteUser(@PathVariable UUID userId) {
//userService.deleteUserWithOauth(userId);
return ResponseEntity.ok(Map.of("message", "해당 사용자가 시스템에서 완전히 삭제되었습니다."));
adminUserService.deleteUser(userId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
package org.sejongisc.backend.admin.dto;

public class AdminUserRequest {
import org.sejongisc.backend.user.entity.Role;
import org.sejongisc.backend.user.entity.UserStatus;

}
/**
* 관리자 페이지의 사용자 필터링/검색 조회 요청
*/
public record AdminUserRequest (
String keyword, // 키워드
Integer generation, // 기수
Role role, // 권한
UserStatus status // 상태
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.sejongisc.backend.admin.dto;

import lombok.Builder;
import org.sejongisc.backend.user.entity.*;

import java.util.UUID;

@Builder
public record AdminUserResponse(
UUID id,
String studentId,
String name,
String email,
String phoneNumber,
long point, // Account 엔티티의 balance 값
Grade grade,
Role role,
UserStatus status,
Integer generation,
String college,
String department,
String teamName,
String positionName
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sejongisc.backend.admin.dto;

/**
* 엑셀 동기화 결과 응답
*/
public record ExcelSyncResponse(
int createdCount,
int updatedCount
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.sejongisc.backend.admin.dto;

import lombok.Builder;

/**
* 엑셀 파일의 한 행 데이터를 담는 Row
*/
@Builder
public record UserExcelRow(
String studentId,
String name,
String phone,
String teamName,
String generation,
String college,
String department,
String grade,
String position,
String gender
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.sejongisc.backend.admin.repository;

import org.sejongisc.backend.admin.dto.AdminUserResponse;
import org.sejongisc.backend.point.entity.AccountType;
import org.sejongisc.backend.user.entity.Role;
import org.sejongisc.backend.user.entity.User;
import org.sejongisc.backend.user.entity.UserStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.UUID;

/**
* 관리자 화면용 User 조회 전용 레포지토리
* - User 도메인의 기본 CRUD는 user 패키지의 UserRepository에서 담당
* - 해당 클래스는 Admin 페이지에서만 사용되는 조회 쿼리와 Admin DTO로의 프로젝션 로직을 정의
*/
public interface AdminUserRepository extends JpaRepository<User, UUID> {

@Query("""
SELECT new org.sejongisc.backend.admin.dto.AdminUserResponse(
u.userId,
u.studentId,
u.name,
u.email,
u.phoneNumber,
COALESCE(a.balance, 0),
u.grade,
u.role,
u.status,
u.generation,
u.college,
u.department,
u.teamName,
u.positionName
)
FROM User u
LEFT JOIN Account a
ON u.userId = a.ownerId
AND a.type = :accountType
WHERE
(:keyword IS NULL OR
u.name LIKE %:keyword% OR
u.email LIKE %:keyword% OR
(:numericKeyword IS NOT NULL AND (
u.studentId LIKE %:numericKeyword% OR
u.phoneNumber LIKE %:numericKeyword%
))
)
AND (:generation IS NULL OR u.generation = :generation)
AND (:role IS NULL OR u.role = :role)
AND (:status IS NULL OR u.status = :status)
ORDER BY u.generation DESC, u.name ASC
""")
List<AdminUserResponse> findAllByAdminFilter(
@Param("keyword") String keyword,
@Param("numericKeyword") String numericKeyword,
@Param("generation") Integer generation,
@Param("role") Role role,
@Param("status") UserStatus status,
@Param("accountType") AccountType accountType
);
}
Loading