CalendarProjects Next

SJ.CHO·2024년 10월 16일

개발공통사항

Lv.0

필수 기능

사용기술

  • SQL
  • DataBase

진행과정

1. API 명세서

Web API 명세서 링크(클릭)

2. ERD 작성하기

3. SQL 작성하기

  • 공통칼럼 :
    • created_at / modified_at : 각각의 ROW의 작성/수정 시간
Create TABLE IF NOT EXISTS users
(
    id          int PRIMARY KEY AUTO_INCREMENT,
    username    varchar(20)  NOT NULL UNIQUE,
    email       varchar(50)  NOT NULL UNIQUE,
    password    varchar(255) NOT NULL,
    role        varchar(50)  NOT NULL,
    created_at  DATETIME,
    modified_at DATETIME
);
  • users : 일정을 작성하는 유저의 Table
    • id : 유저 고유값인 PK
    • email : 유저가 로그인할때 사용하는 EMail 문자열 UNIQUE 특성을 통해 중복 불가능
    • username : 유저가 앱 내부에서 사용하는 닉네임, 마찬가지로 유니크 사용
    • role : 인가 기능을 활용하기 위한 유저 권한 부여 필드 관리자/유저 두개의 권한이 존재
CREATE TABLE IF NOT EXISTS schedules
(
    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
    title           VARCHAR(50)  NOT NULL,
    scheduledetails TEXT         NOT NULL,
    weather         VARCHAR(255) NOT NULL,
    created_at      DATETIME,
    modified_at     DATETIME,
    user_id         BIGINT,
    FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
  • schedules : 일정이 저장되는 Table
    • id : 일정 고유값인 PK
    • title : 일정의 제목
    • scheduledetails : 일정의 상세내용
    • weather : 일정이 생성되는 일자의 날씨
    • user_id : 유저의 고유 PK 를 받아서 연관관계를 맺는 FK
      ON DELETE CASCADE 를 통해 영속성 전이를 통한 삭제 지원
CREATE TABLE IF NOT EXISTS comment
(
    id          BIGINT AUTO_INCREMENT PRIMARY KEY,
    comment     VARCHAR(100) NOT NULL,
    created_at  DATETIME,
    modified_at DATETIME,
    schedule_id BIGINT,
    user_name   VARCHAR(20),
    FOREIGN KEY (schedule_id) REFERENCES schedule (id) ON DELETE CASCADE
);
  • comments : 댓글이 저장되는 Table
    • id : 댓글 고유값인 PK
    • comment : 댓글의 내용
    • user_name : 댓글 작성자의 닉네임
    • schedule_id : 일정의 고유 PK 를 받아서 연관관계를 맺는 FK
      ON DELETE CASCADE 를 통해 영속성 전이를 통한 삭제 지원
CREATE TABLE IF NOT EXISTS userschedule
(
    id          BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id     BIGINT,
    schedule_id BIGINT,
    role        VARCHAR(255) NOT NULL,
    joined_at   DATETIME,
    FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
    FOREIGN KEY (schedule_id) REFERENCES schedules (id) ON DELETE CASCADE
);
  • userschedule : 일정유저M : N 관계를 설정하기 위한 중간 Table
    • id : Row 값의 PK
    • user_id : 해당 일정의 관계되는유저
    • schedule_id : 관계되는 일정의 id
    • role : 관계되는 유저의 역할로 작성자/담당자 두 개가 존재
    • joined_at : 해당 유저가 일정의 언제 관계됐는지 기록
    • schedule_id/user_id : 각각의 고유 PK 를 받아서 연관관계를 맺는 FK
      ON DELETE CASCADE 를 통해 영속성 전이를 통한 삭제 지원

필수구현 Lv.1~5 , 도전구현 Lv.1~4

기능

사용기술

  • MySQL
  • DataBase
  • SpringBoot
  • JWT
  • JPA
  • GRADLE

진행과정

1. JPA Auditing

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable {
    @CreatedDate
    @Column(updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime modifiedAt;
}
  • 일정 , 유저 , 댓글 은 공통적으로 생성/수정일 을 가짐.
  • JPA Auditing 기능을 통해 영속성 컨텍스트의 생성, 수정될경우 자동적으로 시간을 작성하여 저장해주는 기능 구현.
  • 해당 Auditable Class 를 상속하는 Entity Class 들은 Auditing 기능을 통해 생성/수정일 을 관리하게 됌.

2. 회원 가입 / 로그인

회원가입

Controller
@PostMapping("/registration")
  public ResponseEntity<UserCreateResponseDto> createUser(@Valid @RequestBody UserCreateRequestDto ucrDto) {
        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body(userService.createUser(ucrDto));
}
================================================================================================================================================
Service
    public UserCreateResponseDto createUser(UserCreateRequestDto ucrDto) {
        if (userRepository.existsByEmail(ucrDto.getEmail())) {
            throw new CustomException(ALREADY_EMAIL_USER);
        }
        if (userRepository.existsByUserName(ucrDto.getUserName())) {
            throw new CustomException(ALREADY_USERNAME_USER);
        }
        User user = UserCommand.Create.toEntity(ucrDto, passwordEncoder, ADMIN_KEY);
        userRepository.save(user);
        return userMapper.userToUserCreateResponseDto(user);
    }
================================================================================================================================================
Command
    public class UserCommand {

    public static class Create {
        private static String userName;
        private static String email;
        private static String passWord;
        private static UserRole userRole;

        public static User toEntity(UserCreateRequestDto ucrDto, PasswordEncoder pwEncoder, String adKey) {
            userName = ucrDto.getUserName();
            email = ucrDto.getEmail();
            passWord = pwEncoder.encode(ucrDto.getPassWord());
            userRole = UserRole.USER;
            if (ucrDto.isAdmin()) {
                if (!adKey.equals(ucrDto.getAdminKey())) {
                    throw new CustomException(ADMIN_KEY_MISMATCH);
                }
                userRole = UserRole.ADMIN;
            }
            return User.builder()
                    .userName(userName)
                    .email(email)
                    .passWord(passWord)
                    .role(userRole)
                    .build();
        }
    }
  • 요구조건 :
    • 유저들의 이메일은 DB에 암호화 되어서 보관되어야함.
    • 유저 최초 생성(회원가입) 시 JWT를 발급 후 반환
      • 이긴한데 회원가입은 등록 이지 인증 의 영역이 아니기에 생략했음. (튜터님들의 말론 올바른 방식은 아니라고 로그인이 동작한다면 필요없어보인다.)
    • PasswordEncoder 를 이용해 DB에 암호화하여 저장.
    • 동일한 알고리즘으로 해석이 가능하기에 로그인 과정에 문제는 없다.
    • AdminKey 를 알맞게 입력한 유저는 Admin 권한 부여.

로그인

Controller
    @PostMapping("/login")
    public ResponseEntity<UserLoginResponseDto> logIn(@Valid @RequestBody UserLoginRequestDto ulrDto, HttpServletResponse res) {
        UserLoginResponseDto resDto = userService.logIn(ulrDto);
        String token = jwtUtil.createToken(resDto.getId(), resDto.getRole());
        jwtUtil.addJwtToCookie(token, res);
        resDto.setToken(token);
        return ResponseEntity.status(HttpStatus.OK).body(resDto);
    }
================================================================================================================================================
JWTUtil
    // 토큰 생성
    public String createToken(Long id, UserRole role) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setHeaderParam("typ", "JWT")
                        .setSubject(id.toString()) // 사용자 식별자값(ID)
                        .claim(AUTHORIZATION_KEY, role)
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
                        .setIssuedAt(date) // 발급일
                        .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                        .compact();
    }

    // JWT Cookie 에 저장
    public void addJwtToCookie(String token, HttpServletResponse res) {
        try {
            token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행

            Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
            cookie.setPath("/");

            // Response 객체에 Cookie 추가
            res.addCookie(cookie);
        } catch (UnsupportedEncodingException e) {
            logger.error(e.getMessage());
        }
    }
        // JWT 토큰 substring
    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
            return tokenValue.substring(7);
        }
        logger.error("Not Found Token");
        throw new NullPointerException("Not Found Token");
    }

    public Optional<Cookie[]> getCookies(HttpServletRequest req) {
        return Optional.ofNullable(req.getCookies());
    }

    // HttpServletRequest 에서 Cookie Value : JWT 가져오기
    public String getTokenFromRequest(HttpServletRequest req) {
        Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
                    try {
                        return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
                    } catch (UnsupportedEncodingException e) {
                        return null;
                    }
                }
            }
        }
        return null;
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException | SignatureException e) {
            logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            logger.error("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }
================================================================================================================================================
Filter
@Component
@Order(1)
@RequiredArgsConstructor
public class AuthFilter extends OncePerRequestFilter {
    private final JwtUtil jwtUtil;


    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException {
        if (isFilterApplicable(req)) {
            chain.doFilter(req, res);
        }
        String tokenValue = jwtUtil.getTokenFromRequest(req);
        if (!StringUtils.hasText(tokenValue)) {
            throw new UnAuthorizationException(TOKEN_NOT_FOUND);
        }
        String accessToken = jwtUtil.substringToken(tokenValue);
        if (!jwtUtil.validateToken(accessToken)) {
            throw new UnAuthorizationException(INVALID_TOKEN);
        }
        chain.doFilter(req, res);
    }

    private boolean isFilterApplicable(HttpServletRequest req) {
        String path = req.getRequestURI();
        return path.startsWith("/user/registration") || path.startsWith("/user/login") || path.startsWith("/api");
    }
}

  • 로그인 성공시 유저의 고유 ID(PK) 와 권한 정보를 토큰의 담는다.

  • 이 후 유저는 로그인 인증이 필요한 API요청을 호출할때 마다 Filter 를 통해 토큰 검증작업을 거쳐 해당유저의 정보를 확인한다.

  • HandlerMethodArgumentResolver 을 사용하여 이후 로그인이 필요한 처리에서 로그인 유저의 정보가 필요할 시 컨트롤러단에서 토큰을 통해 유저 객체를 생성하여 파라메터로 사용한다.
    (해당 항목은 트러블슈팅 문서에 소개)

3. 일정 CRUD

생성

Controller
    @PostMapping()
    public ResponseEntity<ScheduleCreateResponseDto> createsSchedule(@LoginUser User user, @Valid @RequestBody ScheduleCreateRequestDto scrDto) {
        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body(scheduleService.createSchedule(user, scrDto));
    }
 ================================================================================================================================================
Serivce
public ScheduleCreateResponseDto createSchedule(User user, ScheduleCreateRequestDto scrDto) {
        Schedule schedule = ScheduleCommand.Create.toSchedule(scrDto, user, weatherService);
        scheduleRepository.save(schedule);
        UserSchedule userSchedule = ScheduleCommand.Create.toUserSchedule(schedule);
        userScheduleRepository.save(userSchedule);
        return scheduleMapper.scheduleToScheduleCreateResponseDto(schedule);
    }
================================================================================================================================================
Command
public static class Create {
        private static String title;
        private static String scheduleDetails;
        private static String weather;

        public static Schedule toSchedule(ScheduleCreateRequestDto scrDto, User user, WeatherService weatherService) {
            title = scrDto.getTitle();
            scheduleDetails = scrDto.getScheduleDetails();
            weather = weatherService.getWeather();
            return Schedule.builder()
                    .user(user)
                    .title(title)
                    .scheduleDetails(scheduleDetails)
                    .weather(weather)
                    .build();
        }

        public static UserSchedule toUserSchedule(Schedule schedule) {
            return UserSchedule.builder()
                    .user(schedule.getUser())
                    .schedule(schedule)
                    .role("creator")
                    .joinedAt(LocalDateTime.now())
                    .build();
        }
    }
   ================================================================================================================================================
WeatherService
@Service
public class WeatherService {
    private final RestTemplate restTemplate;

    public WeatherService(RestTemplateBuilder builder) {
        this.restTemplate = builder.build();
    }

    public String getWeather() {
        LocalDate today = LocalDate.now();
        String date = today.format(DateTimeFormatter.ofPattern("MM-dd"));
        URI uri = UriComponentsBuilder
                .fromUriString("https://f-api.github.io")
                .path("/f-api/weather.json")
                .encode()
                .build()
                .toUri();
        ResponseEntity<String> response = restTemplate.getForEntity(uri, String.class);
        return fromJSONtoItems(response.getBody()).stream().filter(weatherResponseDto -> weatherResponseDto.getDate().equals(date)).map(WeatherResponseDto::getWeather).findFirst().orElseThrow(NullPointerException::new);
    }

    public List<WeatherResponseDto> fromJSONtoItems(String responseEntity) {
        JSONArray items = new JSONArray(responseEntity);
        List<WeatherResponseDto> weatherList = new ArrayList<>();

        for (Object item : items) {
            WeatherResponseDto weatherDto = new WeatherResponseDto((JSONObject) item);
            weatherList.add(weatherDto);
        }
        return weatherList;
    }
}

  • 요구조건 :
    • 고유 유저 ID , 할일 제목, 할일 내용, 날씨, 작성/수정일 을 지님.
    • 로그인한 유저의 정보를 가져와 사용함으로 따로 UserId 를 받아올필요가 없다.
    • 날씨의 경우 일정 생성일(MM-DD) 기준으로 연동된 외부 API의 정보를 가져와서 저장.

중간테이블 생성

public static UserSchedule toUserSchedule(Schedule schedule) {
   return UserSchedule.builder()
         .user(schedule.getUser())
         .schedule(schedule)
         .role("creator")
         .joinedAt(LocalDateTime.now())
         .build();
        }
    }
   

  • 일정의 추가담당자를 배치하기위한 Table
  • 생성된 일정의 ID 와 일정을 생성한 유저의 ID를 FK 로 받아 작성한다.

담당자 유저 배치

Controller
    @PostMapping("/assign_user")
    public ResponseEntity<UserScheduleAssignResponseDto> assignUser(@LoginUser User user, @RequestBody @Valid UserScheduleAssignRequestDto uarDto) {
        return ResponseEntity.status(HttpStatus.OK)
                .body(userScheduleService.assignUser(user, uarDto));
    }
================================================================================================================================================
 Service
        public UserScheduleAssignResponseDto assignUser(User user, UserScheduleAssignRequestDto uarDto) {
        UserSchedule userSchedule = UserScheduleCommand.Create.toUserSchedule(uarDto, scheduleRepository, userRepository);
        if (userSchedule.isValidateCreator(user.getId())) {
            throw new CustomException(NOT_CREATOR);
        }
        if (userScheduleRepository.existsByUserIdAndScheduleId(userSchedule.getUser().getId(), userSchedule.getSchedule().getId())) {
            throw new CustomException(ALREADY_ASSIGN_USER);
        }
        userScheduleRepository.save(userSchedule);
        return userScheduleMapper.UserScheduleToUserScheduleDto(userSchedule);
    }
================================================================================================================================================
Command
        public static class Create {
        public static UserSchedule toUserSchedule(UserScheduleAssignRequestDto uarDto, ScheduleRepository scheduleRepository, UserRepository userRepository) {
            Schedule schedule = scheduleRepository.findById(uarDto.getScheduleId()).orElseThrow(() -> new CustomException(SCHEDULE_NOT_FOUND));
            User assignUser = userRepository.findById(uarDto.getAssignUserId()).orElseThrow(() -> new CustomException(USER_NOT_FOUND));
            return UserSchedule.builder()
                    .schedule(schedule)
                    .user(assignUser)
                    .role("assignee")
                    .joinedAt(LocalDateTime.now())
                    .build();
        }
    }
  • 일정의 담당자를 배치하기 위해 해당 유저가 작성자인가.
  • 추가하려는 유저 및 일정이 존재하는지.
  • 추가하려는 유저가 이미 배치되었는지 확인한다.

담당자 유저 삭제

Controller
    @DeleteMapping("/assign_user")
    public ResponseEntity<UserScheduleDeleteUserResponseDto> deleteAssignUser(@LoginUser User user, @RequestBody @Valid UserScheduleDeleteUserRequestDto sduDto) {
        return ResponseEntity.status(HttpStatus.OK)
                .body(userScheduleService.deleteUser(user, sduDto));
    }
================================================================================================================================================    
Service
        public UserScheduleDeleteUserResponseDto deleteUser(User user, UserScheduleDeleteUserRequestDto sduDto) {
        UserSchedule userSchedule = userScheduleRepository.findByUserIdAndScheduleId(sduDto.getDeleteUserId(), sduDto.getScheduleId()).orElseThrow(() -> new CustomException(USER_SCHEDULE_NOT_FOUND));
        if (userSchedule.isValidateCreator(user.getId())) {
            throw new CustomException(NOT_CREATOR);
        }
        if (!userScheduleRepository.existsByUserIdAndRole(userSchedule.getUser().getId(), userSchedule.getRole())) {
            throw new CustomException(ASSIGN_USER_NOT_FOUND);
        }
        UserScheduleDeleteUserResponseDto sdrDto = userScheduleMapper.UserScheduleToUserScheduleDeleteDto(userSchedule);
        userScheduleRepository.delete(userSchedule);
        return sdrDto;
    }
  • 일정의 담당자를 삭제하는 API

  • 조건은 생성과 동일함.

  • 1번 일정의 3번 유저가 담당자에서 삭제됌.

조회

Controller
@GetMapping()
public ResponseEntity<Page<ScheduleReadPageResponseDto>> getSchedules(@RequestParam(defaultValue = "0", value = "pageNo") int pageNo
            , @RequestParam(defaultValue = "10", value = "pageSize") int pageSize) {
        return ResponseEntity
                .status(HttpStatus.OK)
                .body(scheduleService.getSchedules(pageNo, pageSize));
    }
================================================================================================================================================
Service
public Page<ScheduleReadPageResponseDto> getSchedules(int pageNo, int pageSize) {
        PageRequest pageRequest = PageRequest.of(pageNo, pageSize, Sort.Direction.DESC, "modifiedAt");
        Page<Schedule> schedules = scheduleRepository.findAll(pageRequest);
        return schedules.map(schedule -> {
            ScheduleReadPageResponseDto srrDto = scheduleMapper.scheduleToScheduleReadPageResponseDto(schedule);
            srrDto.setCommentCount((long) schedule.getCommentList().size());
            return srrDto;
        });
    }
{
{
    "content": [
        {
            "id": 4,
            "userId": 3,
            "title": "뭐하지",
            "scheduleDetails": "뭐하지뭐하지뭐하지뭐하지뭐하지뭐하지",
            "commentCount": 2,
            "createdAt": "2024-10-16T17:58:25.29407",
            "modifiedAt": "2024-10-16T17:58:25.29407"
        },
        {
            "id": 3,
            "userId": 2,
            "title": "자기",
            "scheduleDetails": "자기자기자기자기자기자기자기",
            "commentCount": 2,
            "createdAt": "2024-10-16T17:57:46.008849",
            "modifiedAt": "2024-10-16T17:57:46.008849"
        },
        {
            "id": 2,
            "userId": 1,
            "title": "놀기",
            "scheduleDetails": "놀기놀기놀기놀기놀기놀기놀기놀기",
            "commentCount": 3,
            "createdAt": "2024-10-16T17:56:20.172406",
            "modifiedAt": "2024-10-16T17:56:20.172406"
        },
        {
            "id": 1,
            "userId": 1,
            "title": "과제하기",
            "scheduleDetails": "스프링과제하기",
            "commentCount": 3,
            "createdAt": "2024-10-16T17:51:19.792219",
            "modifiedAt": "2024-10-16T17:51:19.792219"
        }
    ],
    "pageable": {
        "pageNumber": 0,
        "pageSize": 10,
        "sort": {
            "empty": false,
            "sorted": true,
            "unsorted": false
        },
        "offset": 0,
        "paged": true,
        "unpaged": false
    },
    "last": true,
    "totalElements": 4,
    "totalPages": 1,
    "size": 10,
    "number": 0,
    "sort": {
        "empty": false,
        "sorted": true,
        "unsorted": false
    },
    "first": true,
    "numberOfElements": 4,
    "empty": false
}
  • PageablePageRequest 를 활용해서 Paging 기능을 구현.
  • JSON 내부에 페이지정보 또한 같이 전송하여 클라이언트 측에서 페이지정보 확인이 가능.
  • 댓글 갯수를 카운트해 같이 전달.

수정

Service
public void updateSchedule(Long scheduleId, ScheduleUpdateRequestDto surDto, User user) {
        if (!user.isAdmin()) {
            throw new CustomException(NOT_ADMIN);
        }
        Schedule schedule = scheduleRepository.findById(scheduleId).orElseThrow(() -> new CustomException(SCHEDULE_NOT_FOUND));
        ScheduleCommand.Update.executeUpdate(schedule, surDto);
    }
================================================================================================================================================
Command
    public static class Update {
        private static String title;
        private static String scheduleDetails;

        public static void executeUpdate(Schedule schedule, ScheduleUpdateRequestDto surDto) {
            title = surDto.getTitle();
            scheduleDetails = surDto.getScheduleDetails();
            schedule.setTitle(title);
            schedule.setScheduleDetails(scheduleDetails);
        }
    }
  • 일정의 수정작업은 오직 Admin 계정만 가능함.
  • JPA 의 영속성 컨텍스트를 활용, 굳이 DB에 쿼리를 날리지 않아도 Setter 를 통해 업데이트가 가능하다.

삭제

Service
        public void deleteSchedule(Long scheduleId, User user) {
        if (!user.isAdmin()) {
            throw new CustomException(NOT_ADMIN);
        }
        Schedule schedule = scheduleRepository.findById(scheduleId).orElseThrow(() -> new CustomException(SCHEDULE_NOT_FOUND));
        scheduleRepository.delete(schedule);
    }
  • 일정의 삭제작업은 오직 Admin 계정만 가능.
  • 해당 일정을 삭제할시 연관된 댓글, 담당자 테이블이 함께 삭제된다.

4. 댓글 CRUD

댓글의 경우 특별한 코드가 없기에 코드 생략.

(기본적으로 일정의 CRUD와 유사함.)

생성

  • 일정에 대한 ID 값을 가지며 일정과 동일하게 생성/수정시간을 지님

조회

{
    "scheduleId": 2,
    "commentId": 2,
    "comment": "놀지말고 공부하자",
    "userName": "개발자1",
    "createdAt": "2024-10-16T19:13:43.433908",
    "modifiedAt": "2024-10-16T19:13:43.433908"
}
  • 조회의 경우 단건 조회만 구현 (일정의 단건조회를 통해 댓글 리스트를 볼수있다)

수정

  • 수정된 시간과 수정한 댓글내용 및 유저명 변경.

삭제

  • 연관된 관계가 없기에 자기자신만 삭제된다.

5. 유저 CRUD

  • 생성의 경우 회원가입과 동일한 목적을 가지기에 생략.
  • 로그인기능을 최대한 활용 해보기위해 본인기준의 API 요청만 소개

조회

    @GetMapping("/my_profile")
    public ResponseEntity<UserReadResponseDto> getMyProfile(@LoginUser User user) {
        return ResponseEntity
                .status(HttpStatus.OK)
                .body(userService.getUser(user.getId()));
    }
================================================================================================================================================
        @GetMapping("/my_profile")
    public ResponseEntity<UserReadResponseDto> getMyProfile(@LoginUser User user) {
        return ResponseEntity
                .status(HttpStatus.OK)
                .body(userService.getUser(user.getId()));
    }
{
    "id": 1,
    "userName": "개발자1",
    "email": "abc@gmail.com",
    "role": "ADMIN",
    "schedules": [
        {
            "createdAt": "2024-10-16T17:56:20.172406",
            "modifiedAt": "2024-10-16T17:56:20.172406",
            "id": 2,
            "title": "놀기",
            "scheduleDetails": "놀기놀기놀기놀기놀기놀기놀기놀기",
            "weather": "Rainy",
            "commentList": [
                {
                    "createdAt": "2024-10-16T19:15:24.848497",
                    "modifiedAt": "2024-10-16T19:15:24.848497",
                    "id": 6,
                    "comment": "같이놀기 ㄱㄱㄱ",
                    "userName": "개발자2"
                },
                {
                    "createdAt": "2024-10-16T19:17:28.912455",
                    "modifiedAt": "2024-10-16T19:17:28.912455",
                    "id": 10,
                    "comment": "놀고싶다",
                    "userName": "개발자3"
                }
            ]
        }
    ]
}
  • 자신이 가진 일정의 List 와 일정에 묶여있는 댓글들을 조회한다.

수정

    public void updateUser(User user, UserUpdateRequestDto uurDto) {
        if (userRepository.existsByEmail(uurDto.getEmail())) {
            throw new CustomException(ALREADY_EMAIL_USER);
        }
        if (userRepository.existsByUserName(uurDto.getUserName())) {
            throw new CustomException(ALREADY_USERNAME_USER);
        }
        UserCommand.Update.executeUpdate(user, uurDto);
    }
  • 수정할 닉네임, 이메일이 DB상의 존재하면 수정불가능.

삭제

    @DeleteMapping("/{userId}")
    public ResponseEntity<Void> deleteUser(@LoginUser User user, @PathVariable @Positive(message = "UserId 는 0보다 커야합니다.") Long kickUserId) {
        userService.kickUser(user, kickUserId);
        return ResponseEntity
                .status(HttpStatus.NO_CONTENT)
                .build();
    }

    @DeleteMapping("/my_profile")
    public ResponseEntity<Void> deleteMyProfile(@LoginUser User user) {
        userService.deleteUser(user.getId());
        return ResponseEntity
                .status(HttpStatus.NO_CONTENT)
                .build();
    }
    
================================================================================================================================================

        public void deleteUser(Long userId) {
        User user = userRepository.findById(userId).orElseThrow(() -> new CustomException(USER_NOT_FOUND));
        userRepository.delete(user);
    }

    public void kickUser(User user, Long kickUserId) {
        if (!user.isAdmin()) {
            throw new CustomException(NOT_ADMIN);
        }
        if (kickUserId.equals(user.getId())) {
            throw new CustomException(NOT_KICK_SELF);
        }
        User kickUser = userRepository.findById(kickUserId).orElseThrow(() -> new CustomException(USER_NOT_FOUND));
        userRepository.delete(kickUser);
    }
  • 회원탈퇴 기능과 강퇴기능을 구현. 강퇴의 경우 Admin 권한자만 가능하다.
  • 유저가 삭제되면서 연관관계를 가지는 테이블 전부 삭제
  • 2번 유저 삭제를 통해 연관관계 컬럼들이 삭제됌.

트러블 슈팅

링크(클릭)

구현기능

Web API 명세서 링크(클릭)

Stacks

Architecture

📦 
├─ .gitignore
├─ build.gradle
├─ gradle
│  └─ wrapper
│     ├─ gradle-wrapper.jar
│     └─ gradle-wrapper.properties
├─ gradlew
├─ gradlew.bat
├─ schedule.sql
├─ settings.gradle
└─ src
   ├─ main
   │  ├─ java
   │  │  └─ com
   │  │     └─ sparta
   │  │        └─ calendarprojectsnext
   │  │           ├─ CalendarProjectsNextApplication.java
   │  │           └─ domain : 요청 기능 별 Domain Layer
   │  │              ├─ audit : JPA Auditing 관련 패키지
   │  │              │  └─ Auditable.java
   │  │              ├─ client : 외부요청 API 관련 패키지
   │  │              │  ├─ dto
   │  │              │  │  └─ WeatherResponseDto.java : 날씨 데이터 응답 DTO
   │  │              │  └─ service
   │  │              │     └─ WeatherService.java : 외부 날씨 API 요청 Service
   │  │              ├─ comment : 댓글 관련 패키지
   │  │              │  ├─ command
   │  │              │  │  └─ CommentCommand.java : RequestDto -> Entity 변환 유틸 Class
   │  │              │  ├─ controller
   │  │              │  │  └─ CommentController.java : 댓글 API 컨트롤러
   │  │              │  ├─ dto : 댓글 관련 요청/응답 DTO
   │  │              │  │  ├─ CommentCreateRequestDto.java
   │  │              │  │  ├─ CommentCreateResponseDto.java
   │  │              │  │  ├─ CommentReadResponseDto.java
   │  │              │  │  └─ CommentUpdateRequestDto.java
   │  │              │  ├─ entity : 댓글 Entity
   │  │              │  │  └─ Comment.java
   │  │              │  ├─ mapper : Entity -> ResponseDto 변환 인터페이스
   │  │              │  │  └─ CommentMapper.java
   │  │              │  ├─ repository : 댓글 DB 접근 레파지토리
   │  │              │  │  └─ CommentRepository.java
   │  │              │  └─ service : 댓글 비즈니스로직 서비스
   │  │              │     └─ CommentService.java
   │  │              ├─ config : Spring 환경설정 관련 클래스
   │  │              │  ├─ PasswordEncoder.java : 비밀번호 암호화용 클래스
   │  │              │  └─ WebConfig.java : Resolver 인터페이스 등록용 클래스
   │  │              ├─ exception : 예외 관련 패키지
   │  │              │  ├─ CustomException.java : 사용자 예외 발생 클래스
   │  │              │  ├─ UnAuthorizationException.java : 토큰 인증처리 예외 클래스(필터용)
   │  │              │  ├─ controller : 예외발생용 컨트롤러
   │  │              │  │  └─ GlobalExceptionHandler.java : 전체 예외 Handler 클래스
   │  │              │  ├─ dto : 에러DTO 클래스
   │  │              │  │  └─ ErrorDto.java
   │  │              │  └─ eunm : 사용자 에러 지정 EUNM 클래스
   │  │              │     └─ ErrorCode.java
   │  │              ├─ filter : 인증/인가 용 필터 패키지
   │  │              │  ├─ AuthFilter.java : 인증/인가 용 필터
   │  │              │  └─ AuthenticationExceptionHandlerFilter.java 필터 예외 Handler 클래스
   │  │              ├─ jwt : JWT 관련 유틸 패키지
   │  │              │  └─ JwtUtil.java : JWT 관련 유틸클래스 (생성 검증등)
   │  │              ├─ schedule : 일정 관련 패키지
   │  │              │  ├─ command : RequestDto -> Entity 변환 유틸 Class
   │  │              │  │  └─ ScheduleCommand.java
   │  │              │  ├─ controller : 일정 API 컨트롤러
   │  │              │  │  └─ ScheduleController.java
   │  │              │  ├─ dto : 일정 관련 요청/응답 DTO
   │  │              │  │  ├─ ScheduleCreateRequestDto.java
   │  │              │  │  ├─ ScheduleCreateResponseDto.java
   │  │              │  │  ├─ ScheduleReadPageResponseDto.java
   │  │              │  │  ├─ ScheduleReadResponseDto.java
   │  │              │  │  └─ ScheduleUpdateRequestDto.java
   │  │              │  ├─ entity : 일정 Entity
   │  │              │  │  └─ Schedule.java
   │  │              │  ├─ mapper : Entity -> ResponseDto 변환 인터페이스
   │  │              │  │  └─ ScheduleMapper.java
   │  │              │  ├─ repository : 일정 DB 접근 레파지토리
   │  │              │  │  └─ ScheduleRepository.java
   │  │              │  └─ service : 일정 비즈니스로직 서비스
   │  │              │     └─ ScheduleService.java
   │  │              ├─ user : 유저 관련 패키지
   │  │              │  ├─ command : RequestDto -> Entity 변환 유틸 Class
   │  │              │  │  └─ UserCommand.java
   │  │              │  ├─ controller : 일정 API 컨트롤러
   │  │              │  │  └─ UserController.java
   │  │              │  ├─ dto : 일정 관련 요청/응답 DTO
   │  │              │  │  ├─ UserCreateRequestDto.java
   │  │              │  │  ├─ UserCreateResponseDto.java
   │  │              │  │  ├─ UserLoginRequestDto.java
   │  │              │  │  ├─ UserLoginResponseDto.java
   │  │              │  │  ├─ UserReadResponseDto.java
   │  │              │  │  └─ UserUpdateRequestDto.java
   │  │              │  ├─ entity : 일정 Entity
   │  │              │  │  └─ User.java
   │  │              │  ├─ eunm : 유저 권한 EUNM 클래스
   │  │              │  │  └─ UserRole.java
   │  │              │  ├─ mapper : Entity -> ResponseDto 변환 인터페이스
   │  │              │  │  └─ UserMapper.java
   │  │              │  ├─ repository : 유저 DB 접근 레파지토리
   │  │              │  │  └─ UserRepository.java
   │  │              │  ├─ resolver : HandlerMethodArgumentResolver 사용을 위한 인터페이스 및 클래스
   │  │              │  │  ├─ LoginUserResolver.java : Resolver 구현클래스
   │  │              │  │  └─ util
   │  │              │  │     └─ LoginUser.java : @LoginUser 어노테이션 구현
   │  │              │  └─ service : 유저 비즈니스로직 서비스
   │  │              │     └─ UserService.java
   │  │              └─ userschedule : 일정담당자 관련 패키지
   │  │                 ├─ command : RequestDto -> Entity 변환 유틸 Class
   │  │                 │  └─ UserScheduleCommand.java
   │  │                 ├─ controller : 일정담당자 API 컨트롤러
   │  │                 │  └─ UserScheduleController.java
   │  │                 ├─ dto : 담당자 관련 요청/응답 DTO
   │  │                 │  ├─ UserScheduleAssignRequestDto.java
   │  │                 │  ├─ UserScheduleAssignResponseDto.java
   │  │                 │  ├─ UserScheduleDeleteUserRequestDto.java
   │  │                 │  └─ UserScheduleDeleteUserResponseDto.java
   │  │                 ├─ entity : 담당자 Entity
   │  │                 │  └─ UserSchedule.java
   │  │                 ├─ mapper : Entity -> ResponseDto 변환 인터페이스
   │  │                 │  └─ UserScheduleMapper.java
   │  │                 ├─ repository : 담당자 DB 접근 레파지토리
   │  │                 │  └─ UserScheduleRepository.java
   │  │                 └─ service : 담당자 비즈니스로직 서비스
   │  │                    └─ UserScheduleService.java
   │  └─ resources
   │     └─ application.properties
   └─ test
      └─ java
         └─ com
            └─ sparta
               └─ calendarprojectsnext
                  └─ CalendarProjectsNextApplicationTests.java

©generated by Project Tree Generator

Github 링크

링크(클릭)

profile
70살까지 개발하고싶은 개발자

0개의 댓글