spring_2기[본캠프]/과제

[과제]일정 관리 앱 2 구현

minwoo95 2026. 1. 12. 13:12

개발 전, 공통 조건

  • 모든 테이블은 고유 식별자(ID)를 가집니다.
  • 3 Layer Architecture 에 따라 각 Layer의 목적에 맞게 개발합니다.
  • CRUD 필수 기능은 모두 데이터베이스 연결 및 JPA를 사용해서 개발합니다.
  • 인증/인가 절차는 Cookie/Session을 활용하여 개발합니다.
  • JPA 연관관계는 단방향 입니다. 정말 필요한 경우에만 양방향을 적용합니다. 

Lv 0. API 명세 및 ERD 작성 필수

  • [ ] API 명세서 작성하기
    • [ ] API명세서는 프로젝트 root(최상위) 경로의 README.md 에 작성
    • 참고) API 명세서 작성 가이드
      • API 명세서란 API명, 요청 값(파라미터), 반환 값, 인증/인가 방식, 데이터 및 전달 형식 등 API를 정확하게 호출하고 그 결과를 명확하게 해석하는데 필요한 정보들을 일관된 형식으로 기술한 것을 의미합니다.
      • request 및 response는 json(링크) 형태로 작성합니다.
      예) [서점] 책 API 설계하기
      • 예시
      API 명세서 추천 무료 Tool
    • Document your APIs in Postman | Postman Learning Center
  • [ ] ERD 작성하기
    • [ ] ERD는 프로젝트 root(최상위) 경로의 README.md 에 첨부
    • 참고) ERD 작성 가이드출처: https://online.visual-paradigm.com/ko/community/share/er-diagram-for-online-book-store-1gnrscfbme
      • API 명세 작성을 통해 서비스의 큰 흐름과 기능을 파악 하셨다면 이제는 기능을 구현하기 위해 필요한 데이터가 무엇인지 생각해봐야합니다.
        • 이때, 구현해야 할 서비스의 영역별로 필요한 데이터를 설계하고 각 영역간의 관계를 표현하는 방법이 있는데 이를 ERD(Entity Relationship Diagram)라 부릅니다.
      • ERD 작성간에 다음과 같은 항목들을 학습합니다.
        • E(Entity. 개체)
          • 구현 할 서비스의 영역에서 필요로 하는 데이터를 담을 개체를 의미합니다.
            • ex) 책, 저자, 독자, 리뷰
        • A(Attribute. 속성)
          • 각 개체가 가지는 속성을 의미합니다.
            • ex) 책은 제목, 언어, 출판일, 저자, 가격 등의 속성을 가질 수 있습니다.
        • R(Relationship. 관계)
          • 개체들 사이의 관계를 정의합니다.
            • ex) 저자는 여러 권의 책을 집필할 수 있습니다. 이때, 저자와 책의 관계는 일대다(1:N) 관계입니다.
      ERD 추천 무료 Tool
    • ERDCloud
    • ERD 추천 영상
    • https://www.youtube.com/watch?v=jsOPr3QfMW0

Lv 1. 일정 CRUD 필수

  • [ ] 일정을 생성, 전체 조회, 단건 조회, 수정, 삭제할 수 있습니다.
  • [ ] 일정은 아래 필드를 가집니다.
    • [ ] 작성 유저명, 할일 제목, 할일 내용, 작성일, 수정일 필드
    • [ ] 작성일, 수정일 필드는 JPA Auditing을 활용합니다.

Lv 2. 유저 CRUD 필수

  • [ ] 유저를 생성, 전체 조회, 단건 조회, 수정, 삭제할 수 있습니다.
  • [ ] 유저는 아래와 같은 필드를 가집니다.
    • [ ] 유저명, 이메일, 작성일, 수정일 필드
    • [ ] 작성일, 수정일 필드는 JPA Auditing을 활용합니다.
  • [ ] 연관관계 구현
    • [ ] 일정은 이제 작성 유저명 필드 대신 유저 고유 식별자 필드를 가집니다.

Lv 3. 회원가입 필수

  • [ ] 유저에 비밀번호 필드를 추가합니다.
    • 비밀번호는 8글자 이상이어야합니다.
    • 비밀번호 암호화는 도전 기능에서 수행합니다.

Lv 4. 로그인(인증) 필수

  • [ ] 설명
    • [ ] Cookie/Session을 활용해 로그인 기능을 구현합니다.
  • [ ] 조건
    • [ ] 이메일과 비밀번호를 활용해 로그인 기능을 구현합니다.
    • [ ] 필요한 API들에서 세션을 활용합니다.

Lv 5. 다양한 예외처리 도전

  • [ ] Validation을 활용해 다양한 예외처리를 적용합니다.
    • [ ] @RestControllerAdvice를 활용하여 validation 에러 상황을 클라이언트에게 전달합니다.
  • [ ] 정해진 예외처리 항목이 있는 것이 아닌 프로젝트를 분석하고 예외사항을 지정해 봅니다.
    • [ ] Ex) 할일 제목은 10글자 이내, 유저명은 4글자 이내

Lv 6. 비밀번호 암호화 도전

  • [ ] Lv.3에서 추가한 비밀번호 필드에 들어가는 비밀번호를 암호화합니다.
    • [ ] 암호화를 위한 PasswordEncoder를 직접 만들어 사용합니다.
      • PasswordEncoder 참고 코드
        1. build.gradle 에 아래의 의존성을 추가해주세요.
        2. implementation 'at.favre.lib:bcrypt:0.10.2'
        3. config 패키지가 없다면 추가하고, 아래의 클래스를 추가해주세요.
        4. import at.favre.lib.crypto.bcrypt.BCrypt; import org.springframework.stereotype.Component; @Component public class PasswordEncoder { public String encode(String rawPassword) { return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rawPassword.toCharArray()); } public boolean matches(String rawPassword, String encodedPassword) { BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword); return result.verified; } }

Lv 7. 댓글 CRUD 도전

  • [ ] 생성한 일정에 댓글을 남길 수 있습니다.
    • [ ] 댓글과 일정은 연관관계를 가집니다. → 3주차 연관관계 매핑 참고!
  • [ ] 댓글을 저장, 전체 조회할 수 있습니다.
  • [ ] 댓글은 아래와 같은 필드를 가집니다.
    • [ ] 댓글 내용, 작성일, 수정일, 유저 고유 식별자, 일정 고유 식별자 필드
    • [ ] 작성일, 수정일 필드는 JPA Auditing을 활용하여 적용합니다.

Lv 8. 일정 페이징 조회 도전

필요한 지식을 직접 검색해서 알아보는 것 또한 개발자의 중요한 소양 중 하나입니다.

Spring Data Jpa에서 지원하는 페이지네이션 방법을 알아보고 적용해 주세요.

  • 키워드
    • 데이터베이스
      • offset / limit : SELECT 쿼리에 적용해서 데이터를 제한 범위에 맞게 조회할 수 있습니다.
    • 페이징
      • Pageable : Spring Data JPA에서 제공되는 페이징 관련 인터페이스입니다.
      • PageRequest : Spring Data JPA에서 제공되는 페이지 요청 관련 클래스입니다.
  • [ ] 일정을 Spring Data JPA의 Pageable과 Page 인터페이스를 활용하여 페이지네이션을 구현
    • [ ] 페이지 번호와 페이지 크기를 쿼리 파라미터로 전달하여 요청하는 항목을 나타냅니다.
    • [ ] 할일 제목, 할일 내용, 댓글 개수, 일정 작성일, 일정 수정일, 일정 작성 유저명 필드를 조회합니다.
    • [ ] 디폴트 페이지 크기는 10으로 적용합니다.
  • [ ] 일정의 수정일을 기준으로 내림차순 정렬합니다.

 

comment.controller.CommentController

package com.example.spring_calendarapptask2.comment.controller;

import com.example.spring_calendarapptask2.comment.dto.CommentRequestDto;
import com.example.spring_calendarapptask2.comment.dto.CommentResponseDto;
import com.example.spring_calendarapptask2.comment.service.CommentService;
import com.example.spring_calendarapptask2.schedule.dto.ScheduleRequestDto;
import com.example.spring_calendarapptask2.schedule.dto.ScheduleResponseDto;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

@RestController
@RequestMapping("/api/comment")
@RequiredArgsConstructor
public class CommentController {
    private final CommentService commentService;

    //로그인 확인
    private void loginSessionCheck(HttpServletRequest request) {
        HttpSession session = request.getSession(false);//세션이 없다고 새로 만들지 않는것으로

        if (session == null || session.getAttribute("loginUser") == null) {
            //인증 실패시
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,"로그인이 필요합니다.");
        }
    }

    //댓글 생성 API
    @PostMapping
    public ResponseEntity<CommentResponseDto> createComment(@Valid @RequestBody CommentRequestDto requestDto, HttpServletRequest request) {
        loginSessionCheck(request);//로그인 하였는지?

        HttpSession session = request.getSession(false);//세션정보를 가져온다
        Long loginUserId = (Long) session.getAttribute("loginUser");//고유 식별자를 꺼내온다

        //생성 서비스 호출
        return ResponseEntity.ok(commentService.createComment(requestDto,loginUserId,requestDto.getScheduleId()));
    }

    //댓글 단건 조회 API
    @GetMapping("/{id}")
    public ResponseEntity<CommentResponseDto> getCommentOne(@PathVariable Long id,HttpServletRequest request) {
        loginSessionCheck(request);//로그인 하였는지?
        //단건 조회 서비스 호출
        return ResponseEntity.ok(commentService.getCommentById(id));
    }

    //댓글 전체 조회 API
    @GetMapping
    public ResponseEntity<List<CommentResponseDto>> getCommentAll(HttpServletRequest request) {
        loginSessionCheck(request);//로그인 하였는지?
        List<CommentResponseDto> responseList = commentService.getComment();//전체 조회 서비스 호출 및 배열에 저장
        return ResponseEntity.ok(responseList);
    }

    //댓글 수정 API
    @PutMapping("/{commentId}")
    public ResponseEntity<CommentResponseDto> updateComment(
            @PathVariable Long commentId,
            @Valid @RequestBody CommentRequestDto requestDto,
            HttpServletRequest request){

        loginSessionCheck(request);//로그인 하였는지?

        //수정 서비스 호출
        return ResponseEntity.ok(commentService.updateComment(commentId, requestDto));
    }

    //댓글 삭제 API
    @DeleteMapping("/{commentId}")
    public ResponseEntity<Long> deleteComment(@PathVariable Long commentId,HttpServletRequest request){
        loginSessionCheck(request);//로그인 하였는지?
        Long deletedId = commentService.deleteComment(commentId);//삭제 서비스 호출
        return ResponseEntity.ok(deletedId);
    }
}

 

comment.dto.CommentRequestDto

package com.example.spring_calendarapptask2.comment.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;

@Getter
public class CommentRequestDto {

    @NotNull(message = "일정 ID는 필수입니다.")
    private Long scheduleId;

    @NotBlank(message = "댓글 내용은 비어있을 수 없습니다.")
    @Size(max = 100,message = "댓글은 100자 이내로 작성 부탁드립니다.")
    private String commentContent;
}

 

comment.dto.CommentResponseDto

package com.example.spring_calendarapptask2.comment.dto;

import com.example.spring_calendarapptask2.comment.entity.CommentEntity;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class CommentResponseDto {
    private Long id;
    private String commentContent;
    private String userName;
    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;

    public CommentResponseDto(CommentEntity comment) {
        this.id = comment.getId();
        this.commentContent = comment.getCommentContent();
        // 작성자 엔티티를 통해 이름을 가져옴
        this.userName = comment.getUserEntity().getUserName();

        // BaseEntity로부터 상속받은 필드들
        this.createdAt = comment.getCreatedDate();
        this.modifiedAt = comment.getLastModifiedDate();
    }
}

 

comment.entity.CommentEntity

package com.example.spring_calendarapptask2.comment.entity;

import com.example.spring_calendarapptask2.comment.dto.CommentRequestDto;
import com.example.spring_calendarapptask2.common.entity.BaseEntity;
import com.example.spring_calendarapptask2.schedule.entity.ScheduleEntity;
import com.example.spring_calendarapptask2.user.entity.UserEntity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class CommentEntity extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false,length = 100)
    private String commentContent;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_Id")
    private UserEntity userEntity;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "schedule_Id")
    private ScheduleEntity scheduleEntity;

    public CommentEntity(CommentRequestDto requestDto, ScheduleEntity scheduleEntity, UserEntity userEntity) {
        this.commentContent = requestDto.getCommentContent();
        this.userEntity = userEntity;
        this.scheduleEntity = scheduleEntity;
    }

    public void updateComment(CommentRequestDto requestDto){
        this.commentContent = requestDto.getCommentContent();
        this.setLastModifiedDate(LocalDateTime.now());//수정 시간 업데이트
    }

}

 

comment.repository.CommentRepository

package com.example.spring_calendarapptask2.comment.repository;

import com.example.spring_calendarapptask2.comment.entity.CommentEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CommentRepository extends JpaRepository<CommentEntity, Long>
{
}

 

comment.service.CommentService

package com.example.spring_calendarapptask2.comment.service;

import com.example.spring_calendarapptask2.comment.dto.CommentRequestDto;
import com.example.spring_calendarapptask2.comment.dto.CommentResponseDto;
import com.example.spring_calendarapptask2.comment.entity.CommentEntity;
import com.example.spring_calendarapptask2.comment.repository.CommentRepository;
import com.example.spring_calendarapptask2.schedule.dto.ScheduleRequestDto;
import com.example.spring_calendarapptask2.schedule.dto.ScheduleResponseDto;
import com.example.spring_calendarapptask2.schedule.entity.ScheduleEntity;
import com.example.spring_calendarapptask2.schedule.reposistory.ScheduleRepository;
import com.example.spring_calendarapptask2.user.entity.UserEntity;
import com.example.spring_calendarapptask2.user.repository.UserRepository;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class CommentService {
    private final ScheduleRepository scheduleRepository;
    private final UserRepository userRepository;
    private final CommentRepository commentRepository;

    //댓글 생성
    @Transactional
    public CommentResponseDto createComment(CommentRequestDto requestDto, Long loginUserId,Long scheduleId) {
        //유저 조회
        UserEntity user = userRepository.findById(loginUserId)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "유저를 찾을 수 없습니다."));

        //댓글 조회
        ScheduleEntity schedule = scheduleRepository.findById(scheduleId)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "일정을 찾을 수 없습니다."));

        //댓글 생성 및 저장
        CommentEntity commentEntity = new CommentEntity(requestDto, schedule, user);
        CommentEntity savedComment = commentRepository.save(commentEntity);

        return new CommentResponseDto(savedComment);
    }

    //댓글 수정
    @Transactional
    public CommentResponseDto updateComment(Long id, CommentRequestDto requestDto) {
        //수정할려는 댓글이 있는지 확인하기
        CommentEntity commentEntity = commentRepository.findById(id).orElseThrow(
                ()->new IllegalArgumentException("해당 댓글이 존재하지 않습니다."));
        //댓글이 있다면 가져와서 데이터를 수정
        commentEntity.updateComment(requestDto);

        //DB에 반영
        return new CommentResponseDto(commentEntity);
    }

    //댓글 삭제
    @Transactional
    public Long deleteComment(Long id) {
        //삭제할려는 댓글이 있는지 확인하기
        CommentEntity commentEntity = commentRepository.findById(id).orElseThrow(
                ()->new IllegalArgumentException("해당 댓글이 존재하지 않습니다."));
        //찾은 객체 삭제 하기
        commentRepository.delete(commentEntity);

        //삭제 성공 리턴하기
        return id;
    }

    //전체 조회
    @Transactional(readOnly = true)
    public List<CommentResponseDto> getComment() {
        //저장소에 있는 모든 댓글 가져오기
        List<CommentEntity> allComment = commentRepository.findAll();

        //댓글이 있는지?
        if(allComment.isEmpty()){
            throw new IllegalArgumentException("등록된 댓글이 없습니다.");
        }

        List<CommentResponseDto> dtos = new ArrayList<>();

        for(CommentEntity commentEntity : allComment){
            dtos.add(new CommentResponseDto(commentEntity));
        }

        //결과 반환
        return dtos;
    }

    //단건 조회
    @Transactional(readOnly = true)
    public CommentResponseDto getCommentById(Long id) {
        CommentEntity commentEntity = commentRepository.findById(id).orElseThrow(
                ()->new IllegalArgumentException("해당 댓글이 존재하지 않습니다."));
        return new CommentResponseDto(commentEntity);
    }
}

 

common.entity.BaseEntity

package com.example.spring_calendarapptask2.common.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@MappedSuperclass // 이 클래스를 상속받는 엔티티들에게 필드를 공유
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;


    protected void setLastModifiedDate(LocalDateTime now) {
        this.lastModifiedDate = now;//수정시 수정시간정보 업데이트
    }
}

 

common.exception.GlobalExceptionHandler

package com.example.spring_calendarapptask2.common.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.server.ResponseStatusException;

import java.util.LinkedHashMap;
import java.util.Map;

@RestControllerAdvice // 모든 컨트롤러에서 발생하는 예외를 여기서 가로챈다
public class GlobalExceptionHandler {

    // ResponseStatusException이 발생하면 이 메서드가 실행
    @ExceptionHandler(ResponseStatusException.class)
    public ResponseEntity<Map<String, Object>> handleResponseStatusException(ResponseStatusException ex) {
        Map<String, Object> body = new LinkedHashMap<>();

        //넣은 순서대로 저장
        //상태 코드
        body.put("status", ex.getStatusCode().value());
        //에러 종류
        body.put("error", ex.getStatusCode().toString());
        //작성한 오류 메세지 출력
        body.put("message", ex.getReason());

        return new ResponseEntity<>(body, ex.getStatusCode());//각종 정보와 상태코드를 리턴
    }
    //IllegalArgumentException이 발생시 해당 메서드 실행
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(IllegalArgumentException ex) {
        Map<String, Object> body = new LinkedHashMap<>();

        body.put("status", HttpStatus.BAD_REQUEST.value()); // 400
        body.put("error", "Bad Request");
        body.put("message", ex.getMessage()); // IllegalArgumentException은 getMessage()를 사용

        return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);//400번대 에러와 메세지 전달
    }
    //MethodArgumentNotValidException이 발생시 해당 메서드 실행
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidationException(MethodArgumentNotValidException ex) {
        Map<String, Object> body = new LinkedHashMap<>();

        body.put("status", HttpStatus.BAD_REQUEST.value()); // 400
        body.put("error", "Validation Error");

        // DTO의 message = "..."내용을 가로채온다
        String errorMessage = ex.getBindingResult()
                .getFieldError()
                .getDefaultMessage();

        body.put("message", errorMessage);

        return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);//400번대 에러와 메세지 전달
    }
}

 

common.config.PasswordEncoder

package com.example.spring_calendarapptask2.config;

import at.favre.lib.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Component;

@Component
public class PasswordEncoder {

    //패스워드 암호화
    public String encode(String rawPassword) {
        return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rawPassword.toCharArray());
    }

    //패스워드 일치 여부 확인
    public boolean matches(String rawPassword, String encodedPassword) {
        BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword);
        return result.verified;
    }
}

 

schedule.controller.ScheduleController

package com.example.spring_calendarapptask2.schedule.controller;

import com.example.spring_calendarapptask2.schedule.dto.ScheduleRequestDto;
import com.example.spring_calendarapptask2.schedule.dto.ScheduleResponseDto;
import com.example.spring_calendarapptask2.schedule.service.ScheduleService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

@RestController
@RequestMapping("/api/schedules")
@RequiredArgsConstructor
public class ScheduleController {

    private final ScheduleService scheduleService;

    //로그인 확인
    private void loginSessionCheck(HttpServletRequest request) {
        HttpSession session = request.getSession(false);//세션이 없다고 새로 만들지 않는것으로

        if (session == null || session.getAttribute("loginUser") == null) {
            //인증 실패시
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,"로그인이 필요합니다.");
        }
    }

    //일정 생성 API
    @PostMapping
    public ResponseEntity<ScheduleResponseDto> createSchedule(@Valid @RequestBody ScheduleRequestDto requestDto, HttpServletRequest request) {
        loginSessionCheck(request);//로그인 확인

        HttpSession session = request.getSession(false);
        Long loginUserId = (Long) session.getAttribute("loginUser");

        return ResponseEntity.ok(scheduleService.createSchedule(requestDto,loginUserId));
    }

    //일정 단건 조회 API
    @GetMapping("/{id}")
    public ResponseEntity<ScheduleResponseDto> getScheduleOne(@PathVariable Long id,HttpServletRequest request) {
        loginSessionCheck(request);//로그인 확인
        return ResponseEntity.ok(scheduleService.getScheduleById(id));
    }

    //일정 전체 조회 API
    @GetMapping
    public ResponseEntity<Page<ScheduleResponseDto>> getScheduleAll(
            @RequestParam(defaultValue = "0") int page, // 디폴트 페이지 번호
            @RequestParam(defaultValue = "10") int size, // 디폴트 페이지 크기 10
            HttpServletRequest request){
        loginSessionCheck(request);//로그인 확인
        Page<ScheduleResponseDto> responsePage = scheduleService.getSchedule(page, size);
        return ResponseEntity.ok(responsePage);//Page<ScheduleResponseDto>타입으로 리턴
    }

    //일정 수정 API
    @PutMapping("/{scheduleId}")
    public ResponseEntity<ScheduleResponseDto> updateSchedule(
            @PathVariable Long scheduleId,
            @Valid @RequestBody ScheduleRequestDto requestDto,
            HttpServletRequest request){

        loginSessionCheck(request);//로그인 확인

        return ResponseEntity.ok(scheduleService.updateSchedule(scheduleId, requestDto));
    }

    //일정 삭제 API
    @DeleteMapping("/{scheduleId}")
    public ResponseEntity<Long> deleteSchedule(@PathVariable Long scheduleId,HttpServletRequest request){
        loginSessionCheck(request);//로그인 확인
        Long deletedId = scheduleService.deleteSchedule(scheduleId);//삭제 실행
        return ResponseEntity.ok(deletedId);
    }
}

 

schedule.dto.ScheduleRequestDto

package com.example.spring_calendarapptask2.schedule.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor // JSON 데이터를 객체로 바꿀 때
public class ScheduleRequestDto {

    @NotBlank(message = "할 일 제목은 필수입니다.")
    @Size(max = 10, message = "할 일 제목은 10글자 이내여야 합니다.")
    private String scheduleTitle;

    @NotBlank(message = "할 일 내용은 필수입니다.")
    private String scheduleContent;

    /*@NotNull(message = "유저 Id는 필수입니다.")
    private Long userId;*/
}

 

schedule.dto.ScheduleResponseDto

package com.example.spring_calendarapptask2.schedule.dto;

import com.example.spring_calendarapptask2.schedule.entity.ScheduleEntity;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class ScheduleResponseDto {
    private Long id; // 식별자 필수
    private String userName;
    private String scheduleTitle;
    private String scheduleContent;
    private LocalDateTime createdDate; // JPA Auditing 활용 필드
    private LocalDateTime lastModifiedDate; // JPA Auditing 활용 필드
    private int commentCount;

    public ScheduleResponseDto(ScheduleEntity entity) {
        this.id = entity.getId();
        this.userName = entity.getUser().getUserName();
        this.scheduleTitle = entity.getScheduleTitle();
        this.scheduleContent = entity.getScheduleContent();
        this.createdDate = entity.getCreatedDate();
        this.lastModifiedDate = entity.getLastModifiedDate();
        this.commentCount = entity.getComments().size();
    }
}

 

schedule.entity.ScheduleEntity

package com.example.spring_calendarapptask2.schedule.entity;

import com.example.spring_calendarapptask2.comment.entity.CommentEntity;
import com.example.spring_calendarapptask2.common.entity.BaseEntity;
import com.example.spring_calendarapptask2.schedule.dto.ScheduleRequestDto;
import com.example.spring_calendarapptask2.user.entity.UserEntity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)//작성일, 수정일 자동화
public class ScheduleEntity extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String scheduleTitle;
    private String scheduleContent;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id") // DB에는 작성자 이름 대신 유저 ID(FK)가 저장
    private UserEntity user;

    @OneToMany(mappedBy = "scheduleEntity", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<CommentEntity> comments = new ArrayList<>();

    public ScheduleEntity(ScheduleRequestDto requestDto, UserEntity user) {
        this.scheduleTitle = requestDto.getScheduleTitle();
        this.scheduleContent = requestDto.getScheduleContent();
        this.user = user;
    }

    public void upDateSchedule(String title, String content) {
        this.scheduleTitle = title;
        this.scheduleContent = content;
        this.setLastModifiedDate(LocalDateTime.now());
    }

}

 

schedule.repository.ScheduleRepository

package com.example.spring_calendarapptask2.schedule.reposistory;

import com.example.spring_calendarapptask2.schedule.entity.ScheduleEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ScheduleRepository extends JpaRepository<ScheduleEntity,Long> {
    Page<ScheduleEntity> findAll(Pageable pageable);
}

 

schedule.service.ScheduleService

package com.example.spring_calendarapptask2.schedule.service;

import com.example.spring_calendarapptask2.schedule.dto.ScheduleRequestDto;
import com.example.spring_calendarapptask2.schedule.dto.ScheduleResponseDto;
import com.example.spring_calendarapptask2.schedule.entity.ScheduleEntity;
import com.example.spring_calendarapptask2.schedule.reposistory.ScheduleRepository;
import com.example.spring_calendarapptask2.user.entity.UserEntity;
import com.example.spring_calendarapptask2.user.repository.UserRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class ScheduleService {

    private final ScheduleRepository scheduleRepository;
    private final UserRepository userRepository;

    //일정 생성
    @Transactional
    public ScheduleResponseDto createSchedule(ScheduleRequestDto requestDto,Long loginUserId) {
        //DTO에 담긴 userId로 실제 유저를 찾기
        UserEntity user = userRepository.findById(loginUserId)
                .orElseThrow(() -> new IllegalArgumentException("해당 유저를 찾을 수 없습니다."));

        //찾은 유저 객체를 일정 엔티티에 토스
        ScheduleEntity scheduleEntity = new ScheduleEntity(requestDto, user);

        ScheduleEntity savedSchedule = scheduleRepository.save(scheduleEntity);
        return new ScheduleResponseDto(savedSchedule);
    }

    //일정 수정
    @Transactional
    public ScheduleResponseDto updateSchedule(Long id, ScheduleRequestDto requestDto) {
        //수정할려는 일정이 있는지 확인하기
        ScheduleEntity scheduleEntity = scheduleRepository.findById(id).orElseThrow(
                ()->new IllegalArgumentException("해당 일정이 존재하지 않습니다."));
        //일정이 있다면 가져와서 데이터를 수정
        scheduleEntity.upDateSchedule(requestDto.getScheduleTitle(), requestDto.getScheduleContent());

        //DB에 반영
        return new ScheduleResponseDto(scheduleEntity);
    }

    //일정 삭제
    @Transactional
    public Long deleteSchedule(Long id) {
        //삭제할려는 일정이 있는지 확인하기
        ScheduleEntity scheduleEntity = scheduleRepository.findById(id).orElseThrow(
                ()->new IllegalArgumentException("해당 일정이 존재하지 않습니다."));
        //찾은 객체 삭제 하기
        scheduleRepository.delete(scheduleEntity);

        //삭제 성공 리턴하기
        return id;
    }

    //전체 조회
    @Transactional(readOnly = true)
    public Page<ScheduleResponseDto> getSchedule(int page, int size) {
        //저장소에 있는 모든 일정 가져오기
        //List<ScheduleEntity> allSchedule = scheduleRepository.findAll();

        //Pageable 객체 생성 (page는 0부터 시작, 수정일 내림차순 정렬)
        Pageable pageable = PageRequest.of(page, size, Sort.by("lastModifiedDate").descending());
        //Repository에서 Page 객체로 조회
        Page<ScheduleEntity> schedulePage = scheduleRepository.findAll(pageable);

        //게시글이 있는지?
        /*
        if(allSchedule.isEmpty()){
            throw new IllegalArgumentException("등록된 일정이 없습니다.");
        }

        //List<ScheduleEntity>->List<ScheduleResponseDto>
        List<ScheduleResponseDto> dtos = new ArrayList<>();

        for(ScheduleEntity scheduleEntity : scheduleRepository.findAll()){
            dtos.add(new ScheduleResponseDto(scheduleEntity));
        }

        //결과 반환
        return dtos;
         */
        //데이터 존재 여부 체크
        if (schedulePage.isEmpty()) {
            throw new IllegalArgumentException("조회할 일정이 없습니다.");
        }

        //Entity Page를 Dto Page로 변환
        return schedulePage.map(ScheduleResponseDto::new);
    }

    //단건 조회
    @Transactional(readOnly = true)
    public ScheduleResponseDto getScheduleById(Long id) {
        ScheduleEntity scheduleEntity = scheduleRepository.findById(id).orElseThrow(
                ()->new IllegalArgumentException("해당 일정이 존재하지 않습니다."));
        return new ScheduleResponseDto(scheduleEntity);
    }
}

user.controller.UserController

package com.example.spring_calendarapptask2.user.controller;

import com.example.spring_calendarapptask2.schedule.dto.ScheduleRequestDto;
import com.example.spring_calendarapptask2.schedule.dto.ScheduleResponseDto;
import com.example.spring_calendarapptask2.schedule.service.ScheduleService;
import com.example.spring_calendarapptask2.user.dto.UserLoginRequestDto;
import com.example.spring_calendarapptask2.user.dto.UserLoginResponseDto;
import com.example.spring_calendarapptask2.user.dto.UserRequestDto;
import com.example.spring_calendarapptask2.user.dto.UserResponseDto;
import com.example.spring_calendarapptask2.user.entity.UserEntity;
import com.example.spring_calendarapptask2.user.repository.UserRepository;
import com.example.spring_calendarapptask2.user.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    //로그인 확인
    private void loginSessionCheck(HttpServletRequest request) {
        HttpSession session = request.getSession(false);//세션이 없다고 새로 만들지 않는것으로

        if (session == null || session.getAttribute("loginUser") == null) {
            //인증 실패시
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,"로그인이 필요합니다.");
        }
    }

    //유저 회원가입 API
    @PostMapping
    public ResponseEntity<UserResponseDto> userSignup(@Valid @RequestBody UserRequestDto requestDto) {
        return ResponseEntity.ok(userService.signup(requestDto));
    }

    //유저 단건 조회 API
    @GetMapping("/{id}")
    public ResponseEntity<UserResponseDto> getUsereOne(@PathVariable Long id) {
        return ResponseEntity.ok(userService.getUserOne(id));
    }

    //유저 로그인 API
    @PostMapping("/login")
    public UserLoginResponseDto login(
            @Valid @RequestBody UserLoginRequestDto loginRequestDto,
            HttpServletRequest request //서블릿 리퀘스트를 받아옵니다.
    ) {
        //서비스에서 이메일/비밀번호 검증 (성공하면 유저 엔티티 반환)
        UserLoginResponseDto responseDto = userService.login(loginRequestDto.getUserEmail(), loginRequestDto.getUserPassword());

        //세션 생성
        //request.getSession()은 세션이 있으면 반환, 없으면 새로 생성
        HttpSession session = request.getSession();

        //세션에 로그인한 유저의 정보를 저장합니다.
        // "loginUser"라는 이름의 바구니에 유저의 ID(Long)를 담기
        session.setAttribute("loginUser", responseDto.getId());

        return responseDto;
    }


    //유저 전체 조회 API
    @GetMapping
    public ResponseEntity<List<UserResponseDto>> getUserAll() {
        List<UserResponseDto> responseList = userService.getUser();
        return ResponseEntity.ok(responseList);
    }

    //유저 수정 API
    @PutMapping("/{userId}")
    public ResponseEntity<UserResponseDto> updateUser(
            @PathVariable Long userId,
            @Valid @RequestBody UserRequestDto requestDto,
            HttpServletRequest request){
        loginSessionCheck(request);//로그인 확인
        return ResponseEntity.ok(userService.updateUser(userId, requestDto));
    }

    //유저 삭제 API
    @DeleteMapping("/{userId}")
    public ResponseEntity<Long> deleteUser(@PathVariable Long userId,HttpServletRequest request){
        loginSessionCheck(request);//로그인 확인
        Long deletedId = userService.deleteUser(userId);
        return ResponseEntity.ok(deletedId);
    }
}

 

user.dto.UserLoginRequestDto

package com.example.spring_calendarapptask2.user.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;

//로그인 요청
@Getter
@NoArgsConstructor
public class UserLoginRequestDto {
    @NotBlank(message = "이메일은 필수 입니다.")
    @Size(min = 7, max = 30,message = "이메일의 크기가 7~30 사이여야 합니다.")
    private String userEmail;
    @NotBlank(message="비밀번호는 필수 입니다.")
    @Size(min=8,message="비밀번호는 최소 8글자 이상이어야 합니다.")
    private String userPassword;
}

user.dto.UserLoginResponseDto

package com.example.spring_calendarapptask2.user.dto;

import com.example.spring_calendarapptask2.user.entity.UserEntity;
import lombok.Getter;

//로그인 응답
@Getter
public class UserLoginResponseDto {
    private Long id;
    private String userName;
    private String massage;

    public UserLoginResponseDto(UserEntity user, String massage) {
        this.id = user.getId();
        this.userName = user.getUserName();
        this.massage = massage;
    }
}

user.dto.UserRequestDto

package com.example.spring_calendarapptask2.user.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;

//회원가입 요청
@Getter
@NoArgsConstructor
public class UserRequestDto {
    @NotBlank(message = "유저명은 필수입니다.")
    @Size(min = 1, max = 4,message = "유저의 이름은 1~4자 사이여야 합니다.")
    private String userName;
    @NotBlank(message = "이메일은 필수 입니다.")
    @Size(min = 7, max = 30,message = "이메일의 크기가 7~30 사이여야 합니다.")
    private String userEmail;
    @NotBlank(message="비밀번호는 필수 입니다.")
    @Size(min=8,message="비밀번호는 최소 8글자 이상이어야 합니다.")
    private String userPassword;
}

user.dto.UserResponseDto

package com.example.spring_calendarapptask2.user.dto;

import com.example.spring_calendarapptask2.user.entity.UserEntity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

import java.time.LocalDateTime;

//회원가입 응답
@Getter
public class UserResponseDto {
    private Long id;
    private String userName;
    private String userEmail;
    private LocalDateTime createdDate;
    private LocalDateTime lastModifiedDate;

    public UserResponseDto(UserEntity entity) {
        this.id = entity.getId();
        this.userName = entity.getUserName();
        this.userEmail = entity.getUserEmail();
        this.createdDate = entity.getCreatedDate();
        this.lastModifiedDate = entity.getLastModifiedDate();
    }
}

user.entity.UserEntity

package com.example.spring_calendarapptask2.user.entity;

import com.example.spring_calendarapptask2.common.entity.BaseEntity;
import com.example.spring_calendarapptask2.user.dto.UserRequestDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class UserEntity extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String userName;
    private String userEmail;
    @Column(updatable = false)
    private String userPassword;


    public UserEntity(UserRequestDto requestDto, String encodedPassword) {
        this.userName = requestDto.getUserName();
        this.userEmail = requestDto.getUserEmail();
        this.userPassword = encodedPassword;
    }

    public void updateUser(UserRequestDto requestDto) {
        this.userName = requestDto.getUserName();
        this.userEmail = requestDto.getUserEmail();
        this.setLastModifiedDate(LocalDateTime.now());
    }
}

user.repository.UserRepository

package com.example.spring_calendarapptask2.user.repository;

import com.example.spring_calendarapptask2.user.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.Repository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<UserEntity, Long> {
    Optional<UserEntity> findByUserEmail(String userEmail);
}

 

user.service.UserService

package com.example.spring_calendarapptask2.user.service;

import com.example.spring_calendarapptask2.config.PasswordEncoder;
import com.example.spring_calendarapptask2.schedule.reposistory.ScheduleRepository;
import com.example.spring_calendarapptask2.user.dto.UserLoginResponseDto;
import com.example.spring_calendarapptask2.user.dto.UserRequestDto;
import com.example.spring_calendarapptask2.user.dto.UserResponseDto;
import com.example.spring_calendarapptask2.user.entity.UserEntity;
import com.example.spring_calendarapptask2.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;//비밀번호 암호화 도구 추가

    //유저 회원가입
    @Transactional
    public UserResponseDto signup(UserRequestDto requestDto) {
        userRepository.findByUserEmail(requestDto.getUserEmail()).ifPresent(u -> {
            throw new IllegalArgumentException("이미 가입된 이메일입니다.");
        });

        String encodedPassword = passwordEncoder.encode(requestDto.getUserPassword());//패스워드 암호화

        UserEntity userEntity = new UserEntity(requestDto,encodedPassword);
        UserEntity savedUser = userRepository.save(userEntity);

        return new UserResponseDto(savedUser);
    }

    //유저 로그인
    @Transactional(readOnly = true)
    public UserLoginResponseDto login(String email, String password) {
        //이메일로 유저 찾기
        UserEntity user = userRepository.findByUserEmail(email)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED,"일치하는 이메일 정보가 없습니다."));

        //비밀번호 대조 password 지금 입력한 비밀번호 user.getUserPassword() DB에 저장된 패스워드
        if (!passwordEncoder.matches(password, user.getUserPassword())) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다.");
        }

        return new UserLoginResponseDto(user,"로그인에 성공하였습니다.");
    }

    //유저 전체 조회
    @Transactional(readOnly = true)
    public List<UserResponseDto> getUser() {
        List<UserEntity> allUser = userRepository.findAll();

        if (allUser.isEmpty()) {
            throw new IllegalArgumentException("등록된 유저가 없습니다.");
        }

        List<UserResponseDto> dtos = new ArrayList<>();
        for (UserEntity userEntity : allUser) {
            dtos.add(new UserResponseDto(userEntity));
        }
        return dtos;
    }

    //유저 단건 조회
    @Transactional(readOnly = true)
    public UserResponseDto getUserOne(Long id) {
        UserEntity userEntity = userRepository.findById(id).orElseThrow(
                ()->new IllegalArgumentException("해당 유저가 존재하지 않습니다."));
        return new UserResponseDto(userEntity);
    }

    //유저 수정
    @Transactional
    public UserResponseDto updateUser(Long id,UserRequestDto requestDto) {
        UserEntity userEntity = userRepository.findById(id).orElseThrow(
                ()->new IllegalArgumentException("존재 하지 않는 유저입니다."));

        userEntity.updateUser(requestDto);

        return new UserResponseDto(userRepository.save(userEntity));
    }

    //유저 삭제
    @Transactional
    public Long deleteUser(Long id) {
        UserEntity userEntity = userRepository.findById(id).orElseThrow(
                ()->new IllegalArgumentException("존재 하지 않는 유저입니다."));

        userRepository.delete(userEntity);

        return id;
    }
}

 

SpringCalendarAppTask2Application

package com.example.spring_calendarapptask2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing//작성일, 수정일 자동화
@SpringBootApplication
public class SpringCalendarAppTask2Application {

    public static void main(String[] args) {
        SpringApplication.run(SpringCalendarAppTask2Application.class, args);
    }

}

 

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '4.0.1'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
description = 'Spring_CalendarAppTask2'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    //Bean Validation (@NotBlank, @Email 등 사용 필수)
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    //비밀번호 암호화
    implementation 'at.favre.lib:bcrypt:0.10.2'

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
    testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

 

 

1.로그인 없이 일정 생성시

 

2.일정이 없기때문에, 당연히 댓글 작성하는것도 불가

로그인 없이 일정 조회 불가

 

3.로그인 없이 전체 유저는 조회가능

 

4.회원가입

 

5.로그인

 

6.일정 생성

 

7.댓글 생성

 

8.일정 단건 조회

 

9.일정 전체 조회

 

10.댓글 단건 조회

 

11.댓글 전체 조회

 

12.특정 유저 조회

 

13.전체 유저 조회

 

14.일정 수정

 

15.댓글 수정

 

16.유저 수정

 

17.일정 삭제

 

18.댓글 삭제(일정이 삭제되면서 함께 댓글이 삭제됨)

 

19.유저 삭제