[BE] [FEAT] 관리자 회원 관리 기능 추가#228
Hidden character warning
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Walkthrough엑셀(.xlsx) 기반 사용자 동기화(검증·파싱·행 단위 매핑·DB 동기화)와 관리자용 사용자 관리 API(필터 조회, 상태·역할 변경, 선배 승격, 강제 삭제)를 추가하고 Apache POI 의존성을 도입했습니다. Changes
Sequence DiagramssequenceDiagram
actor Admin
participant Controller as AdminUserController
participant Service as AdminUserService
participant Sync as AdminUserSyncService
participant Repo as AdminUserRepository
participant UserRepo as UserRepository
participant Account as AccountService
participant Ledger as PointLedgerService
Admin->>Controller: uploadMemberExcel(file)
Controller->>Service: syncUsersFromExcel(file)
Service->>Service: validateFile(file)\nparse Excel rows
Service->>Sync: syncMemberData(rows)
Sync->>UserRepo: deactivateActiveNonAdminUsers()
loop for each row
Sync->>UserRepo: findByStudentId(studentId)
alt user exists
Sync->>UserRepo: save(updatedUser)
else
Sync->>UserRepo: save(newUser)
Sync->>Account: createAccount(newUser)
Sync->>Ledger: issueSignupReward(newUser)
end
end
Sync-->>Service: ExcelSyncResponse(created, updated)
Service-->>Controller: ExcelSyncResponse
Controller-->>Admin: 200 OK
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@backend/build.gradle`:
- Around line 98-100: The POI dependency org.apache.poi:poi-ooxml is pinned at a
vulnerable version (5.3.0); update the dependency declaration that contains
"org.apache.poi:poi-ooxml:5.3.0" to a fixed version (at least 5.4.0, preferably
5.5.1) to remediate CVE-2025-31672 and ensure compatibility with Java 21 /
Spring Boot 3.5.5.
In
`@backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java`:
- Around line 54-58: The `@PreAuthorize` annotation in AdminUserController on
methods getAllUsers (and the similar updateUserStatus) references a non-existent
MANAGER role causing only SYSTEM_ADMIN to have access; update the annotation to
use existing roles (e.g., replace 'MANAGER' with 'PRESIDENT', 'VICE_PRESIDENT',
or 'TEAM_LEADER' as appropriate) or add the MANAGER role to your security/roles
configuration so it is recognized—locate the annotations on the
AdminUserController methods (getAllUsers and updateUserStatus) and either change
the hasAnyRole(...) list to include real roles or ensure MANAGER is defined in
your Role/Authority setup to match the intended access model.
In
`@backend/src/main/java/org/sejongisc/backend/admin/repository/AdminUserRepository.java`:
- Around line 22-58: The LEFT JOIN in findAllByAdminFilter can emit duplicate
User rows when Account has multiple rows per (ownerId, type); either enforce
uniqueness on Account by adding a unique constraint for (ownerId, type) in the
Account entity (e.g., `@UniqueConstraint` on ownerId and type) or change the
repository query to deduplicate (use SELECT DISTINCT new
org.sejongisc.backend.admin.dto.AdminUserResponse(...) or wrap
aggregation/choose a single account per owner), and also add an ORDER BY (e.g.,
ORDER BY u.name or u.studentId) to the same JPQL to ensure deterministic result
ordering; update the AdminUserResponse/Account/AccountType references
accordingly.
In
`@backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserService.java`:
- Around line 116-119: deleteUser currently calls
userRepository.deleteById(userId) but related tables (Account, PointHistory,
UserBet, RefreshToken) reference users only by UUID fields and will leave
orphaned rows; either convert those entities to proper JPA relations with
`@ManyToOne/`@JoinColumn and cascade = CascadeType.ALL on User, or modify
deleteUser to explicitly remove dependent records before deleting the User—e.g.,
within the `@Transactional` deleteUser(UUID userId) call invoke repository deletes
such as accountRepository.deleteByOwnerId(userId),
pointHistoryRepository.deleteByUserId(userId),
userBetRepository.deleteByUserId(userId),
refreshTokenRepository.deleteByUserId(userId) (ensure corresponding repository
methods exist) then call userRepository.deleteById(userId).
In
`@backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserSyncService.java`:
- Around line 41-43: The current AdminUserSyncService code unconditionally
deactivates all users via userRepository.findAllByStatus(UserStatus.ACTIVE),
which risks disabling users not in the uploaded Excel and even SYSTEM_ADMIN;
change the flow to (1) parse the Excel first and build the set of user
identifiers or target cohort(s), (2) compute the precise set to deactivate as
active users that are NOT present in that parsed set (instead of all active
users), and (3) explicitly exclude any SYSTEM_ADMIN or other protected roles
when building that deactivation set; perform these operations inside the
existing transactional method (e.g., syncMemberData) so deactivation happens
only after successful parsing and syncing.
In `@backend/src/main/java/org/sejongisc/backend/user/entity/User.java`:
- Around line 52-54: The grade field in User (private Grade grade) is marked
nullable = false but lacks a builder or persistence-side default; add a safe
default either by annotating the field with Lombok's `@Builder.Default` (e.g.,
private `@Builder.Default` Grade grade = <appropriate default>) or by setting it
in the entity's `@PrePersist` method (ensure User#prePersist sets grade when
null), so User.builder() paths won't violate the DB constraint; update the User
class accordingly (keep createUserWithSignupAndPending unchanged).
🧹 Nitpick comments (8)
backend/src/main/java/org/sejongisc/backend/user/entity/Grade.java (1)
9-17:null체크만 있고isBlank()체크가 누락되어 있습니다.
Gender.fromString과Role.fromPosition은null || isBlank()를 모두 확인하지만,Grade.fromString은null만 확인합니다. 빈 문자열의 경우 결과적으로NEW_MEMBER를 반환하므로 기능적 버그는 아니지만, 파싱 메서드 간 일관성을 맞추는 것이 좋습니다.제안
public static Grade fromString(String gradeStr) { - if (gradeStr == null) return NEW_MEMBER; + if (gradeStr == null || gradeStr.isBlank()) return NEW_MEMBER;backend/src/main/java/org/sejongisc/backend/user/entity/Role.java (1)
14-32:AdminUserSyncService.mapRole()에서Role.fromPosition()호출로 통합
AdminUserSyncService.mapRole()과Role.fromPosition()의 위치 매핑 로직이 완전히 동일합니다.AdminUserSyncService의mapRole()메서드를 제거하고Role.fromPosition()을 직접 호출하도록 변경하면 중복을 제거할 수 있습니다.backend/src/main/java/org/sejongisc/backend/user/entity/Gender.java (1)
7-16:fromString이null을 반환하는 점이 다른 enum 파서와 일관성이 없습니다.
Grade.fromString은NEW_MEMBER,Role.fromPosition은TEAM_MEMBER를 기본값으로 반환하지만,Gender.fromString은 매칭 실패 또는 null 입력 시null을 반환합니다. 현재 코드에서applyExcelData는 null 가능한 Gender 값을 직접 할당하므로, 호출부에서null체크 없이 gender 필드의 메서드를 사용하면 NPE 위험이 있습니다. 성별이 선택적 필드라면 현재 설계가 타당하지만, 다른 enum 파서와의 일관성 차원에서 명시적인 문서화나 호출부의 null-safety 검증을 권장합니다.backend/src/main/java/org/sejongisc/backend/common/auth/service/AuthService.java (1)
51-52: 토큰 길이 로깅은 디버그 레벨이 적합합니다.토큰 길이 로깅은 운영 환경에서 매 로그인마다 INFO로 남을 필요는 없습니다.
log.debug로 변경하는 것을 권장합니다.♻️ 제안
- log.info("created accessToken len={}", accessToken == null ? -1 : accessToken.length()); - log.info("created refreshToken len={}", refreshToken == null ? -1 : refreshToken.length()); + log.debug("created accessToken len={}", accessToken.length()); + log.debug("created refreshToken len={}", refreshToken.length());backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserSyncService.java (1)
111-136: 미사용 private 메서드 3개가 존재합니다.
mapGrade,mapRole,mapGender메서드는updateUserFromRow에서 호출되지 않습니다. 대신Grade.fromString,Role.fromPosition,Gender.fromString정적 메서드가 사용되고 있습니다. 데드 코드를 제거해 주세요.♻️ 제안
- /** - * 회원 등급(정/준회원) 매핑 - */ - private Grade mapGrade(String gradeStr) { - if (gradeStr.contains("정회원")) return Grade.REGULAR_MEMBER; - if (gradeStr.contains("준회원")) return Grade.ASSOCIATE_MEMBER; - return Grade.NEW_MEMBER; - } - - /** - * 직위 텍스트를 통한 시스템 접근 권한(Role) 결정 - */ - private Role mapRole(String position) { - if (position == null || position.isBlank()) return Role.TEAM_MEMBER; - if (position.contains("회장") && !position.contains("부회장")) return Role.PRESIDENT; - if (position.contains("부회장") || position.contains("부대표자")) return Role.VICE_PRESIDENT; - if (position.contains("팀장")) return Role.TEAM_LEADER; - return Role.TEAM_MEMBER; - } - - /** - * 성별 텍스트 변환 - */ - private Gender mapGender(String genderStr) { - if (genderStr == null || genderStr.isBlank()) return null; - if (genderStr.contains("남")) return Gender.MALE; - if (genderStr.contains("여")) return Gender.FEMALE; - return null; - }backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserService.java (2)
31-31:DataFormatter는 thread-safe하지 않습니다.
AdminUserService는 싱글톤 빈이므로DataFormatter인스턴스가 여러 스레드에서 공유됩니다. 동시에 엑셀 업로드가 호출되면 데이터 손상이 발생할 수 있습니다. 메서드 로컬 변수로 변경하는 것을 권장합니다.♻️ 제안
public class AdminUserService { private final UserRepository userRepository; private final AdminUserRepository adminUserRepository; private final AdminUserSyncService adminUserSyncService; - private final DataFormatter formatter = new DataFormatter();
syncUsersFromExcel메서드 내에서 로컬로 생성:public ExcelSyncResponse syncUsersFromExcel(MultipartFile file) { DataFormatter formatter = new DataFormatter(); // ... }
getCellValue에 파라미터로 전달하거나, 메서드 내부에서 생성하도록 변경하세요.
144-168: 엑셀 컬럼 인덱스가 매직 넘버로 하드코딩되어 있습니다.컬럼 인덱스(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)가
buildExcelRow와syncUsersFromExcel에 직접 사용되고 있어, 엑셀 포맷 변경 시 여러 곳을 수정해야 합니다. 상수로 추출하면 유지보수성이 향상됩니다.♻️ 상수 추출 예시
private static final int COL_TEAM = 1; private static final int COL_GENERATION = 2; private static final int COL_NAME = 3; private static final int COL_STUDENT_ID = 4; private static final int COL_PHONE = 5; private static final int COL_COLLEGE = 6; private static final int COL_DEPARTMENT = 7; private static final int COL_GRADE = 8; private static final int COL_POSITION = 9; private static final int COL_GENDER = 10;backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java (1)
63-68: 응답 타입이 일관되지 않습니다.
updateUserStatus(Line 63)와updateUserRole(Line 75),forceDeleteUser(Line 93)은ResponseEntity<?>를 사용하지만,promoteToSenior(Line 85)는ResponseEntity<Void>를 사용합니다. 모두noContent()를 반환하므로ResponseEntity<Void>로 통일하는 것이 좋습니다.♻️ 제안
- public ResponseEntity<?> updateUserStatus( + public ResponseEntity<Void> updateUserStatus( ... - public ResponseEntity<?> updateUserRole( + public ResponseEntity<Void> updateUserRole( ... - public ResponseEntity<?> forceDeleteUser(`@PathVariable` UUID userId) { + public ResponseEntity<Void> forceDeleteUser(`@PathVariable` UUID userId) {Also applies to: 75-80, 85-88, 93-96
| @PreAuthorize("hasAnyRole('SYSTEM_ADMIN', 'MANAGER')") // TODO: 현재 MANAGER role은 존재하지 않음. 변경 필요 | ||
| public ResponseEntity<List<AdminUserResponse>> getAllUsers(@ModelAttribute AdminUserRequest request) { | ||
| // TODO: 페이징 추후 고려 | ||
| return ResponseEntity.ok(adminUserService.findAllUsers(request)); | ||
| } |
There was a problem hiding this comment.
존재하지 않는 MANAGER 역할이 @PreAuthorize에 사용되고 있습니다.
TODO에도 언급되어 있지만, MANAGER 역할이 존재하지 않으면 실질적으로 SYSTEM_ADMIN만 접근 가능합니다. Line 62의 updateUserStatus에도 동일한 문제가 있습니다. 의도된 접근 권한을 명확히 하고, 존재하는 역할(예: PRESIDENT, VICE_PRESIDENT, TEAM_LEADER)로 교체하거나 MANAGER 역할을 추가해야 합니다.
이 이슈를 추적하기 위한 새 이슈를 생성할까요?
🤖 Prompt for AI Agents
In
`@backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java`
around lines 54 - 58, The `@PreAuthorize` annotation in AdminUserController on
methods getAllUsers (and the similar updateUserStatus) references a non-existent
MANAGER role causing only SYSTEM_ADMIN to have access; update the annotation to
use existing roles (e.g., replace 'MANAGER' with 'PRESIDENT', 'VICE_PRESIDENT',
or 'TEAM_LEADER' as appropriate) or add the MANAGER role to your security/roles
configuration so it is recognized—locate the annotations on the
AdminUserController methods (getAllUsers and updateUserStatus) and either change
the hasAnyRole(...) list to include real roles or ensure MANAGER is defined in
your Role/Authority setup to match the intended access model.
There was a problem hiding this comment.
@discipline24
TODO에도 언급했지만 role에 MANAGER가 없어서 확인 필요합니당
SessionRole의 MANAGER를 의도한 거였을까요??
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
backend/src/main/java/org/sejongisc/backend/admin/repository/AdminUserRepository.java
Show resolved
Hide resolved
backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserService.java
Show resolved
Hide resolved
backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserSyncService.java
Outdated
Show resolved
Hide resolved
backend/src/main/java/org/sejongisc/backend/user/entity/User.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In
`@backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java`:
- Around line 93-99: The Swagger description in AdminUserController's `@Operation`
for forceDeleteUser is incorrect — it claims a permanent delete while the
service performs a soft delete (adminUserService.deleteUser /
deleteUserSoftDelete). Update the `@Operation` description to state that this
endpoint performs a soft delete (or uses the service's soft-delete behavior),
ensuring the docs match the actual behavior; edit the description string on the
`@Operation` annotation for the forceDeleteUser method accordingly.
In
`@backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserService.java`:
- Line 31: The DataFormatter instance is not thread-safe and must not be a
singleton field in AdminUserService; change the usage so that DataFormatter is
created locally inside syncUsersFromExcel (or created per call) and pass that
local DataFormatter into getCellValue instead of using the private final field;
update references to DataFormatter (remove the field declaration) and modify
syncUsersFromExcel and getCellValue signatures/uses to accept a DataFormatter
parameter or instantiate one inside getCellValue so each thread gets its own
instance.
In `@backend/src/main/java/org/sejongisc/backend/user/entity/User.java`:
- Around line 106-109: canLogin() currently only blocks UserStatus.OUT (return
this.status != UserStatus.OUT) but the USER_NOT_ACTIVE error message implies all
non-ACTIVE users are blocked; update the error message to reflect that only
withdrawn users cannot log in (e.g., "탈퇴한 회원은 로그인할 수 없습니다") and replace
occurrences where USER_NOT_ACTIVE is thrown/returned so the text matches the
canLogin() logic (search for usages of canLogin() and the USER_NOT_ACTIVE
constant or enum to update the message).
- Around line 131-154: applyExcelData currently accepts nullable inputs from
AdminUserSyncService.updateUserFromRow and writes them directly to DB fields
(notably name annotated `@Column`(nullable = false) and gender which can be null
from Gender.fromString), risking constraint violations or unintended nulls;
update applyExcelData (or its caller) to validate and sanitize inputs before
assignment: ensure name is non-null/non-empty (throw/return error or substitute
a safe default) and map/validate gender (use a safe fallback or throw when
Gender.fromString returns null), while keeping Grade.fromString and
Role.fromPosition behavior unchanged; reference applyExcelData,
AdminUserSyncService.updateUserFromRow, and Gender.fromString when making the
fix.
🧹 Nitpick comments (4)
backend/src/main/java/org/sejongisc/backend/user/service/UserService.java (1)
208-215:deleteUserSoftDelete에서도findUser헬퍼 사용 권장.기존
deleteUserSoftDelete메서드도 동일한findById + orElseThrow패턴을 사용하고 있으므로, 새로 추가된findUser헬퍼로 통일하면 일관성이 향상됩니다.♻️ 리팩토링 제안
`@Transactional` public void deleteUserSoftDelete(UUID userId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + User user = findUser(userId); user.setStatus(UserStatus.OUT); refreshTokenService.deleteByUserId(userId); log.info("회원 softdelete 처리 완료: userId={}", userId); }backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserService.java (2)
137-161: 엑셀 컬럼 인덱스에 매직 넘버 대신 상수를 사용하세요.
getCellValue(row, 4),getCellValue(row, 3)등 여러 곳에서 하드코딩된 컬럼 인덱스를 사용하고 있어, 엑셀 양식이 변경되면 수정 누락이 발생하기 쉽습니다.♻️ 상수 정의 예시
+ private static final int COL_TEAM = 1; + private static final int COL_GENERATION = 2; + private static final int COL_NAME = 3; + private static final int COL_STUDENT_ID = 4; + private static final int COL_PHONE = 5; + private static final int COL_COLLEGE = 6; + private static final int COL_DEPARTMENT = 7; + private static final int COL_GRADE = 8; + private static final int COL_POSITION = 9; + private static final int COL_GENDER = 10;
36-69: 엑셀 파싱 시 Zip Bomb(압축 폭탄) 방어를 명시적으로 설정하세요.
XSSFWorkbook은 ZIP 기반.xlsx파일을 처리하므로, 악의적인 사용자가 극도로 압축된 엑셀 파일을 업로드하여 서버 메모리를 소진시킬 수 있습니다. 현재validateFile()메서드는 파일 크기 검증을 하지 않으며,application.yml에 Spring의MultipartFile최대 크기 제한이 명시적으로 설정되어 있지 않습니다. 다음을 추가하기를 권장합니다:
application.yml에서spring.servlet.multipart.max-file-size와max-request-size명시적 설정validateFile()메서드에 파일 크기 검증 로직 추가- 행 수 상한선 설정 (예:
if (excelRows.size() > MAX_ALLOWED_ROWS))backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java (1)
66-71:ResponseEntity<?>와일드카드 타입을ResponseEntity<Void>로 통일하세요.
promoteToSenior(Line 88)은ResponseEntity<Void>를 반환하는 반면,updateUserStatus와updateUserRole은ResponseEntity<?>를 사용합니다. 본문이 없는 204 응답은 모두ResponseEntity<Void>로 통일하는 것이 Swagger 문서 생성 및 타입 안전성 측면에서 좋습니다.Also applies to: 78-83
backend/src/main/java/org/sejongisc/backend/admin/controller/AdminUserController.java
Outdated
Show resolved
Hide resolved
backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserService.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In
`@backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserService.java`:
- Around line 128-133: The file extension check in validateFile is
case-sensitive; update validateFile in AdminUserService to perform a
case-insensitive check (e.g., getOriginalFilename(), trim it, convert to
lowercase with Locale.ROOT and then call endsWith(".xlsx")) so .XLSX/.Xlsx
variants are accepted, and continue to throw
CustomException(ErrorCode.INVALID_FILE_FORMAT) when the normalized name doesn't
end with ".xlsx".
In
`@backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserSyncService.java`:
- Around line 64-70: The signup reward block (Account userAccount =
accountService.createUserAccount(...);
pointLedgerService.processTransaction(...)) can throw and cause the surrounding
transaction in AdminUserSyncService to roll back; extract the reward logic into
its own transactional boundary or guard it with a try-catch so user updates are
not partially rolled back. Concretely, move the createUserAccount +
processTransaction calls into a new method (e.g., issueSignupReward) annotated
with `@Transactional`(propagation = REQUIRES_NEW) or call them inside a try { ...
} catch(Exception e) { log error with savedUser.getUserId(),
TransactionReason.SIGNUP_REWARD, and AccountEntry references } so failures are
logged/handled and do not abort the main sync loop.
- Around line 52-55: Replace the insecure use of rowData.phone() as the initial
password in AdminUserSyncService where existingUser.orElseGet builds a User with
passwordHash set via passwordEncoder.encode(rowData.phone()); instead generate a
secure random temporary password (or create a one-time email/password reset
token) and store its encoded value in passwordHash, and set a flag on the User
(e.g., requirePasswordChange or needsInitialSetup) so first-login forces a reset
or send a password-setup link to the user; update the user creation logic in the
User.builder() branch and any related authentication/first-login flow to respect
the requirePasswordChange flag.
🧹 Nitpick comments (2)
backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserSyncService.java (1)
107-110:parseGeneration방어 코딩현재 호출 경로상
genStr이null이 될 가능성은 낮지만,null입력 시NullPointerException이 발생합니다. 방어적으로 null 체크를 추가하면 안전합니다.♻️ 개선 제안
private int parseGeneration(String genStr) { + if (genStr == null || genStr.isBlank()) return 0; String clean = genStr.replaceAll("[^0-9]", ""); return clean.isEmpty() ? 0 : Integer.parseInt(clean); }backend/src/main/java/org/sejongisc/backend/admin/service/AdminUserService.java (1)
138-161: 엑셀 컬럼 인덱스가 매직 넘버로 사용되고 있습니다.
getCellValue(row, 4, formatter),getCellValue(row, 3, formatter)등 하드코딩된 인덱스는 엑셀 구조가 변경될 때 유지보수가 어렵습니다. 상수나 enum으로 추출하면 가독성과 유지보수성이 개선됩니다.♻️ 예시
private static final int COL_TEAM = 1; private static final int COL_GENERATION = 2; private static final int COL_NAME = 3; private static final int COL_STUDENT_ID = 4; private static final int COL_PHONE = 5; private static final int COL_COLLEGE = 6; private static final int COL_DEPARTMENT = 7; private static final int COL_GRADE = 8; private static final int COL_POSITION = 9; private static final int COL_GENDER = 10;
Summary by CodeRabbit
새로운 기능
개선 사항
데이터 모델/무결성