
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 : 일정을 작성하는 유저의 Tableid : 유저 고유값인 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 : 일정이 저장되는 Tableid : 일정 고유값인 PKtitle : 일정의 제목scheduledetails : 일정의 상세내용weather : 일정이 생성되는 일자의 날씨user_id : 유저의 고유 PK 를 받아서 연관관계를 맺는 FKON 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 : 댓글이 저장되는 Tableid : 댓글 고유값인 PKcomment : 댓글의 내용user_name : 댓글 작성자의 닉네임schedule_id : 일정의 고유 PK 를 받아서 연관관계를 맺는 FKON 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 관계를 설정하기 위한 중간 Tableid : Row 값의 PKuser_id : 해당 일정의 관계되는유저schedule_id : 관계되는 일정의 idrole : 관계되는 유저의 역할로 작성자/담당자 두 개가 존재joined_at : 해당 유저가 일정의 언제 관계됐는지 기록schedule_id/user_id : 각각의 고유 PK 를 받아서 연관관계를 맺는 FKON DELETE CASCADE 를 통해 영속성 전이를 통한 삭제 지원@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;
}
일정 , 유저 , 댓글 은 공통적으로 생성/수정일 을 가짐.생성/수정일 을 관리하게 됌.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();
}
}
암호화 되어서 보관되어야함.등록 이지 인증 의 영역이 아니기에 생략했음. (튜터님들의 말론 올바른 방식은 아니라고 로그인이 동작한다면 필요없어보인다.)PasswordEncoder 를 이용해 DB에 암호화하여 저장.
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 을 사용하여 이후 로그인이 필요한 처리에서 로그인 유저의 정보가 필요할 시 컨트롤러단에서 토큰을 통해 유저 객체를 생성하여 파라메터로 사용한다.
(해당 항목은 트러블슈팅 문서에 소개)
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 , 할일 제목, 할일 내용, 날씨, 작성/수정일 을 지님.생성일(MM-DD) 기준으로 연동된 외부 API의 정보를 가져와서 저장.public static UserSchedule toUserSchedule(Schedule schedule) {
return UserSchedule.builder()
.user(schedule.getUser())
.schedule(schedule)
.role("creator")
.joinedAt(LocalDateTime.now())
.build();
}
}

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
}
Pageable 과 PageRequest 를 활용해서 Paging 기능을 구현.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);
}
}

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);
}

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

생성/수정시간을 지님{
"scheduleId": 2,
"commentId": 2,
"comment": "놀지말고 공부하자",
"userName": "개발자1",
"createdAt": "2024-10-16T19:13:43.433908",
"modifiedAt": "2024-10-16T19:13:43.433908"
}



@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"
}
]
}
]
}
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);
}

@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);
}


📦
├─ .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