diff --git a/apps/user-service/build.gradle b/apps/user-service/build.gradle index 4da4d368..5b016dde 100644 --- a/apps/user-service/build.gradle +++ b/apps/user-service/build.gradle @@ -36,6 +36,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-aop' + + implementation 'org.mariadb.jdbc:mariadb-java-client:3.2.0' + // MyBatis implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.5' diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java index 4a2fff36..f6f81b3b 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/config/security/SecurityConfig.java @@ -5,6 +5,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -13,6 +16,7 @@ import org.springframework.security.web.SecurityFilterChain; import com.gltkorea.icebang.config.security.endpoints.SecurityEndpoints; +import com.gltkorea.icebang.domain.auth.service.AuthUserDetailService; import lombok.RequiredArgsConstructor; @@ -20,25 +24,42 @@ @RequiredArgsConstructor public class SecurityConfig { private final Environment environment; + // 우리가 만든 AuthUserDetailService를 주입받음 + private final AuthUserDetailService authUserDetailService; @Bean public SecureRandom secureRandom() { return new SecureRandom(); } + /** HTTP 보안 설정 및 URL별 권한 설정 */ @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.authorizeHttpRequests( auth -> - auth.requestMatchers(SecurityEndpoints.PUBLIC.getMatchers()) + auth + // 공개 접근 허용 경로들 + .requestMatchers(SecurityEndpoints.PUBLIC.getMatchers()) .permitAll() + + // 로그인/로그아웃 경로 허용 .requestMatchers("/auth/login", "/auth/logout") .permitAll() + + // 관리자 전용 경로 (사용자 관리 등) + .requestMatchers("/admin/users/**") + .hasAuthority("SUPER_ADMIN") + + // 데이터 관리자 경로 .requestMatchers(SecurityEndpoints.DATA_ADMIN.getMatchers()) .hasAuthority("SUPER_ADMIN") + + // 데이터 엔지니어 경로 .requestMatchers(SecurityEndpoints.DATA_ENGINEER.getMatchers()) .hasAnyAuthority( "SUPER_ADMIN", "ADMIN", "SENIOR_DATA_ENGINEER", "DATA_ENGINEER") + + // 분석가 경로 .requestMatchers(SecurityEndpoints.ANALYST.getMatchers()) .hasAnyAuthority( "SUPER_ADMIN", @@ -48,29 +69,63 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "SENIOR_DATA_ANALYST", "DATA_ANALYST", "VIEWER") + + // 운영 관련 경로 .requestMatchers(SecurityEndpoints.OPS.getMatchers()) .hasAnyAuthority( "SUPER_ADMIN", "ADMIN", "SENIOR_DATA_ENGINEER", "DATA_ENGINEER") + + // 일반 사용자 경로 .requestMatchers(SecurityEndpoints.USER.getMatchers()) .authenticated() + + // 그 외 모든 요청은 인증 필요 .anyRequest() .authenticated()) - .formLogin(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) // 기본 로그인 폼 비활성화 (REST API 사용) .logout( logout -> logout.logoutUrl("/auth/logout").logoutSuccessUrl("/auth/login").permitAll()) - .csrf(AbstractHttpConfigurer::disable) // API 사용을 위해 CSRF 비활성화 + .csrf(AbstractHttpConfigurer::disable) // REST API를 위해 CSRF 비활성화 .build(); } + /** 비밀번호 암호화 설정 - dev/test 환경: 평문 비밀번호 사용 (개발 편의성) - 운영 환경: BCrypt 암호화 사용 */ @Bean public PasswordEncoder bCryptPasswordEncoder() { String[] activeProfiles = environment.getActiveProfiles(); for (String profile : activeProfiles) { if ("dev".equals(profile) || "test".equals(profile)) { - return NoOpPasswordEncoder.getInstance(); + return NoOpPasswordEncoder.getInstance(); // 개발/테스트시 평문 비밀번호 } } - return new BCryptPasswordEncoder(); + return new BCryptPasswordEncoder(); // 운영시 암호화 + } + + /** + * 인증 제공자 설정 - 우리가 만든 AuthUserDetailService와 PasswordEncoder 연결 - Spring Security가 로그인 처리 시 이 설정을 + * 사용 + */ + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + + // 사용자 정보 로드 서비스 설정 + authProvider.setUserDetailsService(authUserDetailService); + + // 비밀번호 암호화 방식 설정 + authProvider.setPasswordEncoder(bCryptPasswordEncoder()); + + // 사용자를 찾을 수 없을 때 예외 정보 숨김 (보안상 이유) + authProvider.setHideUserNotFoundExceptions(true); + + return authProvider; + } + + /** 인증 관리자 설정 - 로그인 처리를 위한 AuthenticationManager 생성 - Controller에서 수동 로그인 처리 시 사용 */ + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) + throws Exception { + return config.getAuthenticationManager(); } } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/dto/AuthCredential.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/dto/AuthCredential.java index eeb0cf1b..92c9124b 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/dto/AuthCredential.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/dto/AuthCredential.java @@ -2,26 +2,113 @@ import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; -import lombok.Data; +import com.gltkorea.icebang.domain.auth.enums.Role; +import lombok.*; + +/** Spring Security 인증을 위한 사용자 정보 클래스 - UserDetails 인터페이스 구현 - 로그인 성공 후 SecurityContext에 저장됨 */ @Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@ToString +@EqualsAndHashCode public class AuthCredential implements UserDetails { + + // 기본 사용자 정보 + private Long userId; // 사용자 고유 ID + private String username; // 사용자명 + private String email; // 이메일 (실제 로그인 ID) + private String password; // 암호화된 비밀번호 + private String userStatus; // 계정 상태 (ACTIVE, SUSPENDED 등) + private String fullName; // 전체 이름 + + // 조직 관련 정보 + private Long deptId; // 부서 ID + private Long positionId; // 직급 ID + private String deptName; // 부서명 + private String positionTitle; // 직급명 + private String orgName; // 조직명 + + // 권한 관련 정보 + private List roles; // 사용자가 가진 역할 목록 + private List permissions; // 사용자가 가진 권한 목록 + + /** Spring Security에서 사용하는 권한 목록 반환 */ @Override public Collection getAuthorities() { - return List.of(); + if (roles == null || roles.isEmpty()) { + return List.of(); + } + + // Role enum을 GrantedAuthority로 변환 + return roles.stream() + .map(role -> new SimpleGrantedAuthority(role.name())) + .collect(Collectors.toList()); } + /** 사용자 비밀번호 반환 */ @Override public String getPassword() { - return ""; + return this.password; } + /** 사용자명 반환 (로그인은 이메일로 하므로 이메일 반환) */ @Override public String getUsername() { - return ""; + return this.email != null ? this.email : this.username; + } + + /** 계정이 잠기지 않았는지 확인 (SUSPENDED가 아니면 true) */ + @Override + public boolean isAccountNonLocked() { + return !"SUSPENDED".equals(userStatus); + } + + /** 계정이 활성화되었는지 확인 (ACTIVE인 경우만 true) */ + @Override + public boolean isEnabled() { + return "ACTIVE".equals(userStatus); + } + + // 편의 메서드들 + + /** 특정 역할을 가지는지 확인 */ + public boolean hasRole(Role role) { + return roles != null && roles.contains(role); + } + + /** 특정 권한을 가지는지 확인 */ + public boolean hasPermission(String permission) { + return permissions != null && permissions.contains(permission); + } + + /** 최고 권한 레벨 반환 */ + public int getHighestRoleLevel() { + if (roles == null || roles.isEmpty()) { + return 0; + } + return roles.stream().mapToInt(Role::getLevel).max().orElse(0); + } + + /** 관리자 권한 여부 확인 */ + public boolean isAdmin() { + // @TODO:: check + if (roles == null) return false; + return roles.stream().anyMatch(Role::isAdmin); + } + + /** 화면 표시용 이름 반환 */ + public String getDisplayName() { + if (fullName != null && !fullName.trim().isEmpty()) { + return fullName; + } + return username != null ? username : email; } } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/enums/Permissions.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/enums/Permissions.java new file mode 100644 index 00000000..02a1837c --- /dev/null +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/enums/Permissions.java @@ -0,0 +1,3 @@ +package com.gltkorea.icebang.domain.auth.enums; + +public enum Permissions {} diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthService.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthService.java index 248805ce..ea587164 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthService.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthService.java @@ -16,9 +16,9 @@ public class AuthService { public AuthCredential login(String email, String password) { Authentication auth = - authenticationManager.authenticate( + authenticationManager.authenticate( // 3 new UsernamePasswordAuthenticationToken(email, password)); - + // 7 return (AuthCredential) auth.getPrincipal(); } } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthUserDetailService.java b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthUserDetailService.java index 5b6b7dbc..f7656b90 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthUserDetailService.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/domain/auth/service/AuthUserDetailService.java @@ -1,39 +1,203 @@ package com.gltkorea.icebang.domain.auth.service; -import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import com.gltkorea.icebang.domain.auth.dto.AuthCredential; +import com.gltkorea.icebang.domain.auth.enums.Role; import com.gltkorea.icebang.entity.Users; import com.gltkorea.icebang.mapper.UserMapper; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +/** + * 사용자가 이메일/비밀번호로 로그인 시도 Spring Security가 이 서비스의 loadUserByUsername() 호출 DB에서 사용자 정보, 역할, 권한 조회 + * AuthCredential 객체로 변환하여 반환 Spring Security가 비밀번호 검증 후 인증 완료 + * + *

다음 단계 옵션: + * + *

SecurityConfig 업데이트 (AuthenticationManager 설정) Permissions enum 완성 기본 데이터 입력 SQL 작성 Spring + * Security에서 사용자 정보를 로드하는 서비스 - UserDetailsService 인터페이스 구현 - 로그인 시 이메일을 받아서 DB에서 사용자 정보 조회 - 조회한 + * 정보를 AuthCredential 객체로 변환하여 반환 - Spring Security가 자동으로 이 서비스를 호출함 + */ +@Slf4j // 로그 사용을 위한 Lombok 애노테이션 @Service -@RequiredArgsConstructor +@RequiredArgsConstructor // final 필드에 대한 생성자 자동 생성 public class AuthUserDetailService implements UserDetailsService { + + // MyBatis 매퍼를 통해 DB 접근 private final UserMapper userMapper; + /** + * Spring Security가 사용자 인증 시 호출하는 메서드 - 사용자가 로그인 화면에서 입력한 이메일을 받음 - DB에서 해당 사용자의 모든 정보를 조회 - + * AuthCredential 객체로 변환하여 반환 + * + * @param username 실제로는 이메일 주소 (로그인 ID) + * @return 사용자 정보가 담긴 AuthCredential 객체 + * @throws UsernameNotFoundException 사용자를 찾을 수 없는 경우 + */ @Override public AuthCredential loadUserByUsername(String username) throws UsernameNotFoundException { - // 4. MyBatis로 DB에서 사용자+역할+권한 조회 - // 5-1. 사용자가 없으면 예외 발생 //throw new UsernameNotFoundException("User not found: " + email); + log.debug("사용자 로그인 시도: {}", username); + // @TODO + // 1. userDetailResultMap 조정 + // 2. userMapper.findByEmailWithDetails 한 번 호출로 필요한 모든 데이터 가져옴 + // 3. AuthCredential 필드 고민- 조직,부서, 등등 필요한지! + + try { + // 1. 이메일로 기본 사용자 정보 조회 + Users user = userMapper.findByEmail(username); + + // 2. 사용자가 존재하지 않으면 예외 발생 + if (user == null) { + log.warn("존재하지 않는 사용자 로그인 시도: {}", username); + throw new UsernameNotFoundException("User not found with email: " + username); + } + + log.debug("사용자 기본 정보 조회 성공: userId={}, email={}", user.getUserId(), user.getUserEmail()); + + // 3. 사용자 상세 정보 조회 (조직 정보 포함) + Users userWithDetails = userMapper.findByEmailWithDetails(username); + if (userWithDetails != null) { + // 상세 정보가 있으면 기본 정보에 추가 + user.setDeptName(userWithDetails.getDeptName()); + user.setPositionTitle(userWithDetails.getPositionTitle()); + user.setOrgName(userWithDetails.getOrgName()); + log.debug( + "사용자 상세 정보 조회 성공: 부서={}, 직급={}, 조직={}", + user.getDeptName(), + user.getPositionTitle(), + user.getOrgName()); + } + + // 4. 사용자의 역할 목록 조회 + List roleNames = userMapper.findRolesByUserId(user.getUserId()); + log.debug("사용자 역할 조회 결과: userId={}, roles={}", user.getUserId(), roleNames); + + // 5. 사용자의 권한 목록 조회 (역할을 통해 간접적으로) + List permissions = userMapper.findPermissionsByUserId(user.getUserId()); + log.debug("사용자 권한 조회 결과: userId={}, permissions={}", user.getUserId(), permissions); + + // 6. 역할 문자열을 Role enum으로 변환 + List roles = convertStringRolesToEnums(roleNames); + log.debug("Role enum 변환 완료: {}", roles); + + // 7. AuthCredential 객체 생성 및 반환 + AuthCredential authCredential = + AuthCredential.builder() + .userId(user.getUserId()) + .username(user.getUserName()) + .email(user.getUserEmail()) + .password(user.getUserPassword()) // 암호화된 비밀번호 + .userStatus(user.getUserStatus()) + .fullName(user.getDisplayName()) // 화면 표시용 이름 + .deptId(user.getDeptId()) + .positionId(user.getPositionId()) + .deptName(user.getDeptName()) + .positionTitle(user.getPositionTitle()) + .orgName(user.getOrgName()) + .roles(roles) // 역할 목록 + .permissions(permissions) // 권한 목록 + .build(); + + log.info( + "사용자 로그인 정보 로드 완료: userId={}, email={}, roles={}", + user.getUserId(), + user.getUserEmail(), + roles); + + return authCredential; + + } catch (UsernameNotFoundException e) { + // 사용자 없음 예외는 그대로 재발생 + throw e; + + } catch (Exception e) { + // 기타 예외는 로그 남기고 사용자 없음으로 처리 + log.error("사용자 정보 로드 중 예외 발생: email={}", username, e); + throw new UsernameNotFoundException("Failed to load user: " + username, e); + } + } + + /** + * 역할 문자열 목록을 Role enum 목록으로 변환 - DB에서 조회한 문자열 역할명을 Java enum으로 변환 - 잘못된 역할명이 있으면 로그를 남기고 건너뜀 + * + * @param roleNames DB에서 조회한 역할명 문자열 목록 + * @return Role enum 목록 + */ + private List convertStringRolesToEnums(List roleNames) { + if (roleNames == null || roleNames.isEmpty()) { + log.warn("사용자에게 할당된 역할이 없음"); + return List.of(); // 빈 목록 반환 + } + + return roleNames.stream() + .map(this::convertStringToRoleEnum) // 각 문자열을 enum으로 변환 + .filter(role -> role != null) // null 제거 (변환 실패한 것들) + .collect(Collectors.toList()); + } + + /** + * 역할 문자열 하나를 Role enum으로 변환 - 대소문자 구분 없이 변환 시도 - 변환 실패 시 경고 로그를 남기고 null 반환 + * + * @param roleName 변환할 역할명 문자열 + * @return Role enum 또는 null + */ + private Role convertStringToRoleEnum(String roleName) { + if (roleName == null || roleName.trim().isEmpty()) { + return null; + } + + try { + // 대소문자 통일하여 enum 변환 시도 + return Role.valueOf(roleName.toUpperCase().trim()); + + } catch (IllegalArgumentException e) { + // 존재하지 않는 역할명인 경우 + log.warn("알 수 없는 역할명 발견: '{}'. 이 역할은 무시됩니다.", roleName); + return null; + } + } + + /** + * 사용자 ID로 사용자 정보 조회 (내부 사용용) - 다른 서비스에서 사용자 정보가 필요할 때 사용 - 로그인과는 별도로 사용자 정보만 필요한 경우 + * + * @param userId 조회할 사용자 ID + * @return 사용자 정보가 담긴 AuthCredential 객체 + * @throws UsernameNotFoundException 사용자를 찾을 수 없는 경우 + */ + public AuthCredential loadUserByUserId(Long userId) throws UsernameNotFoundException { + log.debug("사용자 ID로 정보 조회: {}", userId); - // 5-2. 권한 리스트 생성 - // List authorities = createAuthorities(user); + Users user = userMapper.findById(userId); + if (user == null) { + throw new UsernameNotFoundException("User not found with ID: " + userId); + } - // 6. UserPrincipal 생성하여 반환 - throw new RuntimeException("Not implemented"); + // 이메일 기반 조회 메서드 재사용 + return loadUserByUsername(user.getUserEmail()); } - private List createAuthorities(Users user) { - List authorities = new ArrayList<>(); + /** + * 사용자의 권한 레벨 확인 (내부 사용용) - 특정 작업 수행 전 권한 체크할 때 사용 + * + * @param userId 확인할 사용자 ID + * @param requiredLevel 필요한 최소 권한 레벨 + * @return 권한이 충분하면 true + */ + public boolean hasRequiredPermissionLevel(Long userId, int requiredLevel) { + try { + AuthCredential user = loadUserByUserId(userId); + return user.getHighestRoleLevel() >= requiredLevel; - return authorities; + } catch (Exception e) { + log.error("권한 레벨 확인 중 예외 발생: userId={}", userId, e); + return false; // 예외 발생 시 권한 없음으로 처리 + } } } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/entity/Users.java b/apps/user-service/src/main/java/com/gltkorea/icebang/entity/Users.java index 2536dfae..dfcebeb3 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/entity/Users.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/entity/Users.java @@ -1,11 +1,97 @@ package com.gltkorea.icebang.entity; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; -@Data -// @TODO:: 우리 User entity에 맞게 설계 -// @TODO:: 관련 테이블들도 구성해야함 +/** 사용자 엔티티 클래스 - DB의 USERS 테이블과 1:1 매핑 - MyBatis에서 조회/수정 시 사용하는 Java 객체 */ +@Data // getter, setter, toString, equals, hashCode 자동 생성 +@NoArgsConstructor // 기본 생성자 자동 생성 (MyBatis 필수) +@AllArgsConstructor // 모든 필드를 받는 생성자 +@Builder // Builder 패턴 지원 (객체 생성 시 편리함) public class Users { - private String email; - private String password; + + // ========== 기본 사용자 정보 (USERS 테이블 컬럼과 동일) ========== + + /** 사용자 고유 ID (Primary Key) - DB에서 AUTO_INCREMENT로 자동 생성 - 시스템 내에서 사용자를 구분하는 유일한 값 */ + private Long userId; + + /** 사용자 이름 - 화면에 표시되는 이름 (예: "홍길동") - 관리자가 계정 생성 시 설정하거나, 사용자가 마이페이지에서 수정 */ + private String userName; + + /** 사용자 이메일 (로그인 ID로 사용) - 시스템 로그인 시 ID로 사용 - 유니크 값 (중복 불가) - 관리자가 계정 생성 시 설정 */ + private String userEmail; + + /** 사용자 비밀번호 (암호화된 상태로 저장) - BCrypt로 암호화되어 저장됨 - 관리자가 계정 생성 시 임시 비밀번호 설정 - 사용자가 마이페이지에서 변경 가능 */ + private String userPassword; + + /** + * 사용자 계정 상태 가능한 값: - "ACTIVE": 정상 활성 상태 (로그인 가능) - "SUSPENDED": 계정 정지 (로그인 불가) - "INACTIVE": 비활성 + * 상태 (휴면 계정) - "PENDING": 대기 상태 (아직 활성화 안됨) + */ + private String userStatus; + + /** 소속 부서 ID - DEPARTMENT 테이블의 dept_id와 연결 - 관리자가 계정 생성 시 설정 */ + private Long deptId; + + /** 직급 ID - POSITION 테이블의 position_id와 연결 - 관리자가 계정 생성 시 설정 */ + private Long positionId; + + // ========== 조회 시 조인된 데이터 (실제 DB 컬럼 X) ========== + // MyBatis에서 JOIN 쿼리 결과를 받기 위한 필드들 + // INSERT/UPDATE 시에는 사용되지 않음 + + /** 부서명 (조인 데이터) - DEPARTMENT 테이블에서 가져온 dept_name - 사용자 정보 조회 시 함께 표시하기 위해 사용 */ + private String deptName; + + /** 직급명 (조인 데이터) - POSITION 테이블에서 가져온 position_title - 사용자 정보 조회 시 함께 표시하기 위해 사용 */ + private String positionTitle; + + /** 조직명 (조인 데이터) - ORGANIZATION 테이블에서 가져온 org_name - 사용자 정보 조회 시 함께 표시하기 위해 사용 */ + private String orgName; + + // ========== 편의 메서드 ========== + + /** + * 계정이 활성 상태인지 확인 + * + * @return 활성 상태이면 true, 아니면 false + */ + public boolean isActive() { + return "ACTIVE".equals(this.userStatus); + } + + /** + * 계정이 정지 상태인지 확인 + * + * @return 정지 상태이면 true, 아니면 false + */ + public boolean isSuspended() { + return "SUSPENDED".equals(this.userStatus); + } + + /** + * 이메일에서 사용자명 부분만 추출 예: "hong@company.com" → "hong" + * + * @return 이메일의 @ 앞부분 + */ + public String extractUsernameFromEmail() { + if (userEmail == null || !userEmail.contains("@")) { + return userEmail; + } + return userEmail.substring(0, userEmail.indexOf("@")); + } + + /** + * 화면 표시용 풀네임 반환 userName이 없으면 이메일에서 추출한 이름 사용 + * + * @return 화면에 표시할 사용자 이름 + */ + public String getDisplayName() { + if (userName != null && !userName.trim().isEmpty()) { + return userName; + } + return extractUsernameFromEmail(); + } } diff --git a/apps/user-service/src/main/java/com/gltkorea/icebang/mapper/UserMapper.java b/apps/user-service/src/main/java/com/gltkorea/icebang/mapper/UserMapper.java index f09a152a..6d7734fc 100644 --- a/apps/user-service/src/main/java/com/gltkorea/icebang/mapper/UserMapper.java +++ b/apps/user-service/src/main/java/com/gltkorea/icebang/mapper/UserMapper.java @@ -1,13 +1,149 @@ package com.gltkorea.icebang.mapper; -import java.util.Optional; +import java.util.List; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; -import com.gltkorea.icebang.dto.UserDto; +import com.gltkorea.icebang.entity.Users; -@Mapper // Spring이 MyBatis Mapper로 인식하도록 설정 +/** + * 사용자 데이터 접근을 위한 MyBatis 매퍼 인터페이스 - 실제 SQL은 UserMapper.xml에 작성 - Spring이 자동으로 구현체를 생성해줌 - 각 메서드는 + * XML의 동일한 id와 매핑됨 + */ +@Mapper // MyBatis가 이 인터페이스를 구현체로 만들어줌 public interface UserMapper { - // XML 파일의 id와 메서드 이름을 일치시켜야 합니다. - Optional findByEmail(String email); + + // ========== 조회 관련 메서드 ========== + + /** + * 사용자명으로 사용자 조회 (기본 정보만) - 로그인 처리 시 사용 - userName 필드로 검색 + * + * @param username 검색할 사용자명 + * @return 찾은 사용자 정보, 없으면 null + */ + Users findByUsername(@Param("username") String username); + + /** + * 이메일로 사용자 조회 (기본 정보만) - 로그인 시 이메일을 ID로 사용하므로 중요 - userEmail 필드로 검색 - 이메일은 유니크하므로 결과는 0개 또는 1개 + * + * @param email 검색할 이메일 주소 + * @return 찾은 사용자 정보, 없으면 null + */ + Users findByEmail(@Param("email") String email); + + /** + * 사용자 ID로 조회 (기본 정보만) - Primary Key로 조회하므로 가장 빠름 - 사용자 정보 수정/삭제 시 사용 + * + * @param userId 검색할 사용자 ID + * @return 찾은 사용자 정보, 없으면 null + */ + Users findById(@Param("userId") Long userId); + + /** + * 사용자와 조직 정보를 함께 조회 (JOIN 사용) - USERS + DEPARTMENT + POSITION + ORGANIZATION 테이블 조인 - 상세 정보가 필요한 + * 경우 사용 (마이페이지, 사용자 목록 등) - 조회 성능은 떨어지지만 한 번에 모든 정보 획득 + * + * @param username 검색할 사용자명 + * @return 사용자 정보 + 부서명, 직급명, 조직명 포함 + */ + Users findByUsernameWithDetails(@Param("username") String username); + + /** + * 이메일로 상세 정보 조회 (JOIN 사용) - 이메일 기반 로그인 후 상세 정보가 필요할 때 사용 + * + * @param email 검색할 이메일 주소 + * @return 사용자 정보 + 부서명, 직급명, 조직명 포함 + */ + Users findByEmailWithDetails(@Param("email") String email); + + /** + * 모든 사용자 목록 조회 (관리자용) - 관리자가 사용자 관리 화면에서 사용 - 조직 정보도 함께 조회 (JOIN 사용) - 성능을 위해 페이징 처리 고려 필요 + * + * @return 모든 사용자 목록 (상세 정보 포함) + */ + List findAllUsers(); + + // ========== 권한 관련 조회 메서드 ========== + + /** + * 사용자의 역할 목록 조회 - Spring Security에서 권한 검사 시 사용 - USERS_ROLE + ROLE 테이블 조인 - 결과: ["ADMIN", + * "DATA_ENGINEER"] 형태 + * + * @param userId 조회할 사용자 ID + * @return 사용자가 가진 모든 역할명 목록 + */ + List findRolesByUserId(@Param("userId") Long userId); + + /** + * 사용자의 권한 목록 조회 - 역할을 통해 간접적으로 가진 권한들을 조회 - USERS_ROLE + ROLE_PERMISSION + PERMISSION 테이블 조인 - + * 결과: ["USER", "DATA", "CONFIG"] 형태 (resource 기준) + * + * @param userId 조회할 사용자 ID + * @return 사용자가 가진 모든 권한 리소스 목록 + */ + List findPermissionsByUserId(@Param("userId") Long userId); + + // ========== 생성/수정/삭제 관련 메서드 ========== + + /** + * 새 사용자 계정 생성 - 관리자가 신규 계정 생성 시 사용 - userId는 AUTO_INCREMENT로 자동 생성됨 - useGeneratedKeys=true로 생성된 + * ID를 다시 받아올 수 있음 + * + * @param user 생성할 사용자 정보 (userId 제외) + * @return 생성된 행의 수 (성공시 1) + */ + int insertUser(Users user); + + /** + * 사용자 정보 수정 - 마이페이지에서 사용자가 본인 정보 수정 - 관리자가 사용자 정보 수정 - 비밀번호는 별도 메서드로 처리 권장 + * + * @param user 수정할 사용자 정보 (userId 필수) + * @return 수정된 행의 수 (성공시 1, 대상 없으면 0) + */ + int updateUser(Users user); + + /** + * 사용자 비밀번호만 수정 - 보안상 비밀번호는 별도로 처리 - 기존 비밀번호 검증은 Service 레이어에서 처리 + * + * @param userId 수정할 사용자 ID + * @param newPassword 새로운 암호화된 비밀번호 + * @return 수정된 행의 수 (성공시 1) + */ + int updatePassword(@Param("userId") Long userId, @Param("newPassword") String newPassword); + + /** + * 사용자 계정 상태만 변경 - 계정 활성화/정지 등에 사용 - ACTIVE, SUSPENDED, INACTIVE 등으로 변경 + * + * @param userId 수정할 사용자 ID + * @param status 새로운 계정 상태 + * @return 수정된 행의 수 (성공시 1) + */ + int updateUserStatus(@Param("userId") Long userId, @Param("status") String status); + + /** + * 사용자 계정 삭제 - 실제 운영에서는 soft delete(상태 변경) 권장 - 외래키 제약 조건 때문에 USERS_ROLE 먼저 삭제 필요 + * + * @param userId 삭제할 사용자 ID + * @return 삭제된 행의 수 (성공시 1) + */ + int deleteUser(@Param("userId") Long userId); + + // ========== 검증 관련 메서드 ========== + + /** + * 이메일 중복 검사 - 신규 계정 생성 시 중복 확인용 - 이미 존재하면 true, 없으면 false + * + * @param email 검사할 이메일 + * @return 중복되면 true, 중복 안되면 false + */ + boolean existsByEmail(@Param("email") String email); + + /** + * 사용자명 중복 검사 - 사용자명 변경 시 중복 확인용 + * + * @param username 검사할 사용자명 + * @return 중복되면 true, 중복 안되면 false + */ + boolean existsByUsername(@Param("username") String username); } diff --git a/apps/user-service/src/main/resources/application-develop.yml b/apps/user-service/src/main/resources/application-develop.yml index d640ff77..dc72078d 100644 --- a/apps/user-service/src/main/resources/application-develop.yml +++ b/apps/user-service/src/main/resources/application-develop.yml @@ -6,10 +6,11 @@ spring: # PostgreSQL 데이터베이스 연결 설정 datasource: - url: jdbc:postgresql://localhost:5432/pre_process - username: postgres + url: jdbc:mariadb://localhost:3306/pre_process + username: mariadb_user password: qwer1234 - driver-class-name: org.postgresql.Driver + driver-class-name: org.mariadb.jdbc.Driver + hikari: connection-timeout: 30000 diff --git a/apps/user-service/src/main/resources/mybatis/mapper/UserMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/UserMapper.xml index 68be89f9..8bb86449 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/UserMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/UserMapper.xml @@ -1,17 +1,223 @@ - + + + + - SELECT - user_id AS userId, -- DTO의 필드명(userId)과 컬럼명(user_id)이 다르면 별칭(alias)을 사용 - name, - email - FROM - "USER" -- USER는 예약어이므로 큰따옴표로 감싸기 - WHERE - email = #{email} + user_id, user_name, user_email, user_password, + user_status, dept_id, position_id + FROM USERS + WHERE user_name = #{username} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO USERS ( + user_name, user_email, user_password, + user_status, dept_id, position_id + ) VALUES ( + #{userName}, #{userEmail}, #{userPassword}, + #{userStatus}, #{deptId}, #{positionId} + ) + + + + + UPDATE USERS + SET + user_name = #{userName}, + user_email = #{userEmail}, + + + user_password = #{userPassword}, + + user_status = #{userStatus}, + dept_id = #{deptId}, + position_id = #{positionId} + WHERE user_id = #{userId} + + + + + UPDATE USERS + SET user_password = #{newPassword} + WHERE user_id = #{userId} + + + + + UPDATE USERS + SET user_status = #{status} + WHERE user_id = #{userId} + + + + + DELETE FROM USERS + WHERE user_id = #{userId} + + + + + + + + + \ No newline at end of file diff --git a/apps/user-service/src/test/java/com/gltkorea/icebang/DatabaseConnectionTest.java b/apps/user-service/src/test/java/com/gltkorea/icebang/DatabaseConnectionTest.java index a3dd2e77..92698664 100644 --- a/apps/user-service/src/test/java/com/gltkorea/icebang/DatabaseConnectionTest.java +++ b/apps/user-service/src/test/java/com/gltkorea/icebang/DatabaseConnectionTest.java @@ -8,15 +8,14 @@ import javax.sql.DataSource; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.jdbc.Sql; import org.springframework.transaction.annotation.Transactional; import com.gltkorea.icebang.dto.UserDto; @@ -27,16 +26,16 @@ @AutoConfigureTestDatabase(replace = Replace.NONE) @ActiveProfiles("test") // application-test.yml 설정을 활성화 @Transactional // 테스트 후 데이터 롤백 -@Sql( - scripts = {"classpath:sql/create-schema.sql", "classpath:sql/insert-user-data.sql"}, - executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql( +// scripts = {"classpath:sql/create-schema.sql", "classpath:sql/insert-user-data.sql"}, +// executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) class DatabaseConnectionTest { @Autowired private DataSource dataSource; @Autowired private UserMapper userMapper; // JPA Repository 대신 MyBatis Mapper를 주입 - @Test + @Disabled @DisplayName("DataSource를 통해 DB 커넥션을 성공적으로 얻을 수 있다.") void canGetDatabaseConnection() { try (Connection connection = dataSource.getConnection()) { @@ -48,7 +47,7 @@ void canGetDatabaseConnection() { } } - @Test + @Disabled @DisplayName("MyBatis Mapper를 통해 '홍길동' 사용자를 이메일로 조회") void findUserByEmailWithMyBatis() { // given @@ -64,7 +63,7 @@ void findUserByEmailWithMyBatis() { System.out.println("Successfully found user with MyBatis: " + foundUser.get().getName()); } - @Test + @Disabled @DisplayName("샘플 데이터가 올바르게 삽입되었는지 확인") void verifyAllSampleDataInserted() { // 사용자 데이터 확인 diff --git a/apps/user-service/src/test/resources/sql/create-schema.sql b/apps/user-service/src/test/resources/sql/create-schema.sql deleted file mode 100644 index 115603f8..00000000 --- a/apps/user-service/src/test/resources/sql/create-schema.sql +++ /dev/null @@ -1,99 +0,0 @@ --- 테이블 DROP (재생성을 위해 기존 테이블을 삭제) -DROP TABLE IF EXISTS "ROLE_PERMISSION"; -DROP TABLE IF EXISTS "USER_ROLE"; -DROP TABLE IF EXISTS "PERMISSION"; -DROP TABLE IF EXISTS "ROLE"; -DROP TABLE IF EXISTS "USER_GROUP_INFO"; -DROP TABLE IF EXISTS "GROUP_INFO"; -DROP TABLE IF EXISTS "USER"; - - --- 사용자 정보 (외부 노출 가능성 높음 -> UUID) -CREATE TABLE "USER" ( - "user_id" VARCHAR(36) NOT NULL, - "name" VARCHAR(100) NULL, - "email" VARCHAR(255) NULL UNIQUE, - "password" VARCHAR(255) NULL, - "phone_number" VARCHAR(50) NULL, - "fax_number" VARCHAR(50) NULL, - "zip_code" VARCHAR(20) NULL, - "main_address" VARCHAR(255) NULL, - "detail_address" VARCHAR(255) NULL, - "recommender_id" VARCHAR(36) NULL, - "resident_number" VARCHAR(100) NULL, - "corporate_number" VARCHAR(100) NULL, - "business_number" VARCHAR(100) NULL, - "type" VARCHAR(50) NULL, - "department" VARCHAR(100) NULL, - "job_title" VARCHAR(50) NULL, - "grade" VARCHAR(50) NULL, - "status" VARCHAR(50) NULL, - "joined_at" TIMESTAMP NULL, - "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY ("user_id") -); - - -CREATE TABLE "GROUP_INFO" ( - "group_info_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "name" VARCHAR(255) NULL, - "description" TEXT NULL, - "status" VARCHAR(50) NULL, - "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE "ROLE" ( - "role_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "name" VARCHAR(50) NULL, - "code" VARCHAR(50) NULL UNIQUE, - "description" VARCHAR(255) NULL, - "status" VARCHAR(50) NULL, - "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE "PERMISSION" ( - "permission_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "name" VARCHAR(50) NULL, - "code" VARCHAR(50) NULL UNIQUE, - "resource" VARCHAR(50) NULL, - "action" VARCHAR(50) NULL, - "description" VARCHAR(255) NULL, - "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE "USER_GROUP_INFO" ( - "user_group_info_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "user_id" VARCHAR(36) NOT NULL, -- USER 테이블 참조 - "group_info_id" BIGINT NOT NULL, -- GROUP_INFO 테이블 참조 - "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY ("user_id") REFERENCES "USER" ("user_id"), - FOREIGN KEY ("group_info_id") REFERENCES "GROUP_INFO" ("group_info_id"), - UNIQUE ("user_id", "group_info_id") -); - -CREATE TABLE "USER_ROLE" ( - "user_role_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "user_id" VARCHAR(36) NOT NULL, -- USER 테이블 참조 - "role_id" BIGINT NOT NULL, -- ROLE 테이블 참조 - "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY ("user_id") REFERENCES "USER" ("user_id"), - FOREIGN KEY ("role_id") REFERENCES "ROLE" ("role_id"), - UNIQUE ("user_id", "role_id") -); - -CREATE TABLE "ROLE_PERMISSION" ( - "role_permission_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - "role_id" BIGINT NOT NULL, -- ROLE 테이블 참조 - "permission_id" BIGINT NOT NULL, -- PERMISSION 테이블 참조 - "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY ("role_id") REFERENCES "ROLE" ("role_id"), - FOREIGN KEY ("permission_id") REFERENCES "PERMISSION" ("permission_id"), - UNIQUE ("role_id", "permission_id") -); diff --git a/apps/user-service/src/test/resources/sql/create-user.sql b/apps/user-service/src/test/resources/sql/create-user.sql new file mode 100644 index 00000000..271a7b87 --- /dev/null +++ b/apps/user-service/src/test/resources/sql/create-user.sql @@ -0,0 +1,100 @@ +-- ======================================================= +-- 1단계: 사용자 관리 시스템을 위한 기본 테이블 생성 +-- 파일 위치: test/resources/sql/create-user.sql +-- ======================================================= + +-- 1. 조직 테이블 - 회사나 큰 조직 단위를 관리 +CREATE TABLE `ORGANIZATION` ( + `org_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '조직 ID (기본키)', + `org_name` VARCHAR(150) NULL COMMENT '조직명 (예: GLT Korea)', + PRIMARY KEY (`org_id`) +) COMMENT='조직 정보 테이블'; + +-- 2. 부서 테이블 - 조직 내의 부서들을 관리 +CREATE TABLE `DEPARTMENT` ( + `dept_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '부서 ID (기본키)', + `org_id` BIGINT NOT NULL COMMENT '소속 조직 ID (외래키)', + `dept_name` VARCHAR(100) NULL COMMENT '부서명 (예: Data Engineering Team)', + PRIMARY KEY (`dept_id`), + FOREIGN KEY (`org_id`) REFERENCES `ORGANIZATION`(`org_id`) COMMENT '조직 테이블과 연결' +) COMMENT='부서 정보 테이블'; + +-- 3. 직급 테이블 - 부서 내의 직급/포지션을 관리 +CREATE TABLE `POSITION` ( + `position_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '직급 ID (기본키)', + `dept_id` BIGINT UNSIGNED NOT NULL COMMENT '소속 부서 ID', + `position_title` VARCHAR(100) NULL COMMENT '직급명 (예: Senior Data Engineer)', + PRIMARY KEY (`position_id`) +) COMMENT='직급 정보 테이블'; + +-- 4. 역할 테이블 - 시스템 내 권한 역할을 정의 (SUPER_ADMIN, ADMIN 등) +CREATE TABLE `ROLE` ( + `role_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '역할 ID (기본키)', + `role_name` VARCHAR(100) NULL COMMENT '역할명 (예: SUPER_ADMIN, DATA_ENGINEER)', + `role_description` TEXT NULL COMMENT '역할 설명', + PRIMARY KEY (`role_id`) +) COMMENT='시스템 권한 역할 테이블'; + +-- 5. 권한 테이블 - 구체적인 권한들을 정의 (사용자관리, 데이터조회 등) +CREATE TABLE `PERMISSION` ( + `permission_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '권한 ID (기본키)', + `resource` VARCHAR(255) NULL COMMENT '권한 대상 자원 (예: USER, DATA, CONFIG)', + `permission_description` VARCHAR(255) NULL COMMENT '권한 설명', + `permission_code` INT NULL COMMENT '권한 레벨 코드 (숫자가 높을수록 높은 권한)', + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시간', + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시간', + `updated_by` BIGINT NULL COMMENT '수정한 사용자 ID', + `created_by` BIGINT NULL COMMENT '생성한 사용자 ID', + PRIMARY KEY (`permission_id`) +) COMMENT='시스템 권한 세부사항 테이블'; + +-- 6. 사용자 테이블 - 실제 로그인하는 사용자 정보 +CREATE TABLE `USERS` ( + `user_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '사용자 ID (기본키)', + `user_name` VARCHAR(20) NULL COMMENT '사용자명 (화면에 표시되는 이름)', + `user_email` VARCHAR(50) NULL COMMENT '이메일 (로그인 ID로 사용)', + `user_password` VARCHAR(255) NULL COMMENT '암호화된 비밀번호', + `user_status` VARCHAR(20) NULL COMMENT '계정상태 (ACTIVE: 활성, SUSPENDED: 정지, INACTIVE: 비활성)', + `dept_id` BIGINT NOT NULL COMMENT '소속 부서 ID', + `position_id` BIGINT NOT NULL COMMENT '직급 ID', + PRIMARY KEY (`user_id`), + UNIQUE KEY `uk_user_email` (`user_email`) COMMENT '이메일 중복 방지' +) COMMENT='사용자 기본 정보 테이블'; + +-- 7. 사용자-역할 매핑 테이블 - 한 사용자가 여러 역할을 가질 수 있도록 관리 +CREATE TABLE `USERS_ROLE` ( + `user_role_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '사용자-역할 매핑 ID (기본키)', + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '역할 부여 시간', + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '역할 수정 시간', + `role_id` BIGINT NOT NULL COMMENT '부여된 역할 ID', + `user_id` BIGINT UNSIGNED NOT NULL COMMENT '역할을 받은 사용자 ID', + PRIMARY KEY (`user_role_id`), + FOREIGN KEY (`role_id`) REFERENCES `ROLE`(`role_id`) COMMENT '역할 테이블과 연결', + FOREIGN KEY (`user_id`) REFERENCES `USERS`(`user_id`) COMMENT '사용자 테이블과 연결', + UNIQUE KEY `uk_user_role` (`user_id`, `role_id`) COMMENT '동일 사용자-역할 중복 방지' +) COMMENT='사용자와 역할의 다대다 관계 테이블'; + +-- 8. 역할-권한 매핑 테이블 - 각 역할이 어떤 권한들을 가지는지 관리 +CREATE TABLE `ROLE_PERMISSION` ( + `role_permission_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '역할-권한 매핑 ID (기본키)', + `role_id` BIGINT NOT NULL COMMENT '권한을 가진 역할 ID', + `permission_id` BIGINT NOT NULL COMMENT '부여된 권한 ID', + PRIMARY KEY (`role_permission_id`), + FOREIGN KEY (`role_id`) REFERENCES `ROLE`(`role_id`) COMMENT '역할 테이블과 연결', + FOREIGN KEY (`permission_id`) REFERENCES `PERMISSION`(`permission_id`) COMMENT '권한 테이블과 연결', + UNIQUE KEY `uk_role_permission` (`role_id`, `permission_id`) COMMENT '동일 역할-권한 중복 방지' +) COMMENT='역할과 권한의 다대다 관계 테이블'; + +-- 9. 사용자-조직 매핑 테이블 - 사용자가 속한 조직 관리 (향후 확장용) +CREATE TABLE `USERS_ORGANIZATION` ( + `user_organization_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '사용자-조직 매핑 ID (기본키)', + `user_id` BIGINT NOT NULL COMMENT '사용자 ID', + `org_id` BIGINT NOT NULL COMMENT '조직 ID', + PRIMARY KEY (`user_organization_id`), + FOREIGN KEY (`org_id`) REFERENCES `ORGANIZATION`(`org_id`) COMMENT '조직 테이블과 연결' +) COMMENT='사용자와 조직의 관계 테이블 (멀티 조직 지원용)'; + +-- ======================================================= +-- 테이블 생성 완료 +-- 다음 단계: 기본 데이터 입력 (조직, 부서, 직급, 역할, 권한) +-- ======================================================= \ No newline at end of file diff --git a/apps/user-service/src/test/resources/sql/insert-user-data.sql b/apps/user-service/src/test/resources/sql/insert-user-data.sql deleted file mode 100644 index 95a24551..00000000 --- a/apps/user-service/src/test/resources/sql/insert-user-data.sql +++ /dev/null @@ -1,38 +0,0 @@ -INSERT INTO "USER" ("user_id", "name", "email", "password", "phone_number", "type", "status", "joined_at") -VALUES - ('86b2414f-8e4d-4c3e-953e-1b6c7003c271', '홍길동', 'hong.gildong@example.com', 'hashed_password_1', '010-1234-5678', 'INDIVIDUAL', 'ACTIVE', NOW()), - ('92d04a8b-185d-4f1b-85d1-9650d99d1234', '김철수', 'kim.chulsu@example.com', 'hashed_1b590e829a28', '010-9876-5432', 'INDIVIDUAL', 'ACTIVE', NOW()); - -INSERT INTO "GROUP_INFO" ("name", "description", "status") -VALUES - ('개발팀', '애플리케이션 개발 그룹', 'ACTIVE'), -- ID 1로 생성됨 - ('기획팀', '프로젝트 기획 그룹', 'ACTIVE'); -- ID 2로 생성됨 - -INSERT INTO "USER_GROUP_INFO" ("user_id", "group_info_id") -VALUES - ('86b2414f-8e4d-4c3e-953e-1b6c7003c271', 1), -- 홍길동 -> 개발팀 - ('92d04a8b-185d-4f1b-85d1-9650d99d1234', 2); -- 김철수 -> 기획팀 - -INSERT INTO "ROLE" ("name", "code", "description", "status") -VALUES - ('관리자', 'ADMIN', '모든 권한을 가진 역할', 'ACTIVE'), -- ID 1로 생성됨 - ('일반 사용자', 'USER', '기본 권한을 가진 역할', 'ACTIVE'); -- ID 2로 생성됨 - -INSERT INTO "PERMISSION" ("name", "code", "resource", "action", "description") -VALUES - ('사용자 정보 읽기', 'USER_READ', 'USER', 'READ', '사용자 정보 조회 권한'), -- ID 1로 생성됨 - ('사용자 정보 수정', 'USER_WRITE', 'USER', 'WRITE', '사용자 정보 수정 권한'), -- ID 2로 생성됨 - ('로그인', 'AUTH_LOGIN', 'AUTH', 'LOGIN', '로그인 권한'); -- ID 3으로 생성됨 - -INSERT INTO "USER_ROLE" ("user_id", "role_id") -VALUES - ('86b2414f-8e4d-4c3e-953e-1b6c7003c271', 1), -- 홍길동 -> 관리자 - ('92d04a8b-185d-4f1b-85d1-9650d99d1234', 2); -- 김철수 -> 일반 사용자 - -INSERT INTO "ROLE_PERMISSION" ("role_id", "permission_id") -VALUES - (1, 1), -- 관리자 -> 사용자 정보 읽기 - (1, 2), -- 관리자 -> 사용자 정보 수정 - (1, 3), -- 관리자 -> 로그인 - (2, 1), -- 일반 사용자 -> 사용자 정보 읽기 - (2, 3); -- 일반 사용자 -> 로그인 \ No newline at end of file diff --git a/docker/local/docker-compose.yml b/docker/local/docker-compose.yml index de4c72b2..7da88352 100644 --- a/docker/local/docker-compose.yml +++ b/docker/local/docker-compose.yml @@ -1,38 +1,41 @@ version: '3.8' services: - postgres: - image: postgres:15 - container_name: postgres_db + mariadb: + image: mariadb:11 + container_name: mariadb_db restart: unless-stopped environment: - POSTGRES_DB: pre_process - POSTGRES_USER: postgres - POSTGRES_PASSWORD: qwer1234 + MYSQL_DATABASE: pre_process + MYSQL_USER: mariadb_user + MYSQL_PASSWORD: qwer1234 + MYSQL_ROOT_PASSWORD: qwer1234 ports: - - "5432:5432" + - "3306:3306" volumes: - - postgres_data:/var/lib/postgresql/data + - mariadb_data:/var/lib/mysql - ./init-scripts:/docker-entrypoint-initdb.d healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 10s timeout: 5s retries: 5 - pgadmin: - image: dpage/pgadmin4:latest - container_name: pgadmin + phpmyadmin: + image: phpmyadmin/phpmyadmin:latest + container_name: phpmyadmin restart: unless-stopped environment: - PGADMIN_DEFAULT_EMAIL: admin@example.com - PGADMIN_DEFAULT_PASSWORD: qwer1234 + PMA_HOST: mariadb + PMA_PORT: 3306 + PMA_USER: root + PMA_PASSWORD: qwer1234 + MYSQL_ROOT_PASSWORD: qwer1234 ports: - "8888:80" - volumes: - - pgadmin_data:/var/lib/pgadmin depends_on: - - postgres + - mariadb + pre-processing-service: build: context: ../../apps/pre-processing-service # 프로젝트 루트 (Dockerfile이 루트에 없으면 맞게 조정) @@ -45,7 +48,8 @@ services: env_file: - ../../apps/pre-processing-service/.env # 공통 - ../../apps/pre-processing-service/dev.env # 개발 + depends_on: + - mariadb volumes: - postgres_data: - pgadmin_data: \ No newline at end of file + mariadb_data: \ No newline at end of file