내일배움캠프/TIL

[Spring]_일정관리 Develop 트러블 슈팅

cork-7 2025. 2. 13. 13:41

단계

<필수>

Lv1 - 일정 CRUD(생성,조회,수정,삭제/[필드 - 작성 유저명, 일정 제목, 할 일, 작성일, 수정일])

Lv2 - 유저 CRUD(유저명, 이메일, 작성일 수정일/일정에서 작성유저명을 유저 고유식별자로 변경)

Lv3 - 유저에 비밀번호 추가 

Lv4 - Cookie/Session을 통해 로그인 기능 구현(이메일과 비밀번호 사용)

<도전>

Lv5 - 다양한 예외처리 적용

Lv6 - 비밀번호 암호화

Lv7 - 댓글 CRUD

Lv8 - 일정 페이징 조회

 

-- 구현 단계 Lv 4

구현중 문제점

일정/유저/회원가입/로그인 기능들을 만들고 연결하는 과정에서 지속적인 로그인 실패

(아직 해결단계이며 예측상 일정을 생성할 때 세션이 전달되지않아 발생하는것으로 보임)

 

환경변수

spring.application.name=scheduleDevelop

spring.datasource.url=jdbc:mysql://localhost:3306/Develop
spring.datasource.username=[소스 아이디]
spring.datasource.password=[소스 비밀번호]
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=create // 어플리케이션 실행시 기존 데이터베이스 삭제후 재생성

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect

 

Api 명세서

 

 

ERD

 

BaseEntity

package com.example.scheduledevelop.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 {
	// 일정, 유저등 생성하거나 수정할때 시간을 저장하기에 따로 만들어 extends 시킨다
    
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

}

 

Lv1 일정 CRUD작성

Entity

package com.example.scheduledevelop.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter
@NoArgsConstructor
public class Schedule extends BaseEntity{

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Setter
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    private String title;
    private String todo;


    public Schedule(User user, String title, String todo) {
        this.user = user;
        this.title = title;
        this.todo = todo;
    }

    public void update(String title, String todo){
        this.title = title;
        this.todo = todo;
    }
}

Controller

package com.example.scheduledevelop.controller;

import com.example.scheduledevelop.dto.ScheduleRequestDto;
import com.example.scheduledevelop.dto.ScheduleResponseDto;
import com.example.scheduledevelop.service.ScheduleService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class ScheduleController {

    private final ScheduleService scheduleService;

    @PostMapping("/schedules")
    public ResponseEntity<ScheduleResponseDto> save(@RequestBody ScheduleRequestDto dto){
        return new ResponseEntity<>(scheduleService.save(dto), HttpStatus.CREATED);
    }

    @GetMapping("/schedules")
    public ResponseEntity<List<ScheduleResponseDto>> findAll(){
        return new ResponseEntity<>(scheduleService.findAll(), HttpStatus.OK);
    }

    @GetMapping("/schedules/{id}")
    public ResponseEntity<ScheduleResponseDto> findOne(@PathVariable Long id){
        return new ResponseEntity<>(scheduleService.findById(id), HttpStatus.OK);
    }

    @PutMapping("/schedules/{id}")
    public ResponseEntity<ScheduleResponseDto> update(@PathVariable Long id, @RequestBody ScheduleRequestDto dto){
        return new ResponseEntity<>(scheduleService.update(id, dto), HttpStatus.OK);
    }

    @DeleteMapping("/schedules/{id}")
    public ResponseEntity<Void> deleteSchedule(@PathVariable Long id){
        scheduleService.deleteById(id);
        return ResponseEntity.ok().build(); // 빈 본문을 반환하기 위해 build사용
    }

 

Service

package com.example.scheduledevelop.service;

import com.example.scheduledevelop.dto.ScheduleRequestDto;
import com.example.scheduledevelop.dto.ScheduleResponseDto;
import com.example.scheduledevelop.dto.UserResponseDto;
import com.example.scheduledevelop.entity.Schedule;
import com.example.scheduledevelop.entity.User;
import com.example.scheduledevelop.repository.ScheduleRepository;
import com.example.scheduledevelop.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@RequiredArgsConstructor
public class ScheduleService {

    private final ScheduleRepository scheduleRepository;
    private final UserRepository userRepository;

    @Transactional
    public ScheduleResponseDto save(ScheduleRequestDto dto){
        User user = userRepository.findById(dto.getUserId()).orElseThrow(()-> new IllegalArgumentException("유저를 찾을 수 없습니다"));

        Schedule schedule = new Schedule(user, dto.getTitle(), dto.getTodo());
        Schedule saveSchedule = scheduleRepository.save(schedule);

        return new ScheduleResponseDto(saveSchedule.getId(), user.getName(), saveSchedule.getTitle(), saveSchedule.getTodo());
    }

    @Transactional(readOnly = true)
    public List<ScheduleResponseDto> findAll(){
        List<Schedule> schedules = scheduleRepository.findAll();

        List<ScheduleResponseDto> dtos = new ArrayList<>();
        for(Schedule schedule : schedules){
            ScheduleResponseDto dto = new ScheduleResponseDto(schedule.getId(), schedule.getTitle(), schedule.getTodo());
            UserResponseDto userDto = new UserResponseDto(schedule.getUser().getId(), schedule.getUser().getName());
            dtos.add(dto);
        }
        // 빠른 for문 생성 schedules.for + tab키
        return dtos;
    }

    @Transactional(readOnly = true)
    public ScheduleResponseDto findById(Long id){
        Schedule schedule = scheduleRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("작성된 일정이 없습니다!")
        );
        return new ScheduleResponseDto(schedule.getId(), schedule.getUser().getName(), schedule.getTitle(), schedule.getTodo());
    }

    @Transactional
    public ScheduleResponseDto update(Long id, ScheduleRequestDto dto){
        Schedule schedule = scheduleRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("작성된 일정이 없습니다!")
        );
        schedule.update(dto.getTitle(),dto.getTodo());
        return new ScheduleResponseDto(schedule.getId(), schedule.getUser().getName(), schedule.getTitle(), schedule.getTodo());
    }

    @Transactional
    public void deleteById(Long id){
        if(!scheduleRepository.existsById(id)){
            throw new IllegalArgumentException("작성된 일정이 없습니다!");
        }
        scheduleRepository.deleteById(id);
    }
}

 

Repository

package com.example.scheduledevelop.repository;

import com.example.scheduledevelop.entity.Schedule;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
    List<Schedule> findByUserId(Long userId); // ManyToOne으로 연결한 유저의 아이디를 가져오기 위해 작성
}

 

Lv2 유저 CRUD

package com.example.scheduledevelop.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;


@Entity
@Getter
@NoArgsConstructor
public class User extends BaseEntity{

    @Id@GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;

    private String name;

    private String password;


    public User(String email, String name, String password) {
        this.email = email;
        this.name = name;
        this.password = password;
    }

    public void update(Long id, String email, String name,  String password){
        this.id = id;
        this.email = email;
        this.name = name;
        this.password = password;
    }
}

 

Controller

package com.example.scheduledevelop.controller;

import com.example.scheduledevelop.dto.SignUpRequestDto;
import com.example.scheduledevelop.dto.UserRequestDto;
import com.example.scheduledevelop.dto.UserResponseDto;
import com.example.scheduledevelop.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping("/users/signup")
    public ResponseEntity<UserResponseDto> signup(@RequestBody SignUpRequestDto signUpRequestDto){
        UserResponseDto userResponseDto = userService.signUp(signUpRequestDto);
        return new ResponseEntity<>(userResponseDto, HttpStatus.CREATED);
    }

    @GetMapping("/users")
    public ResponseEntity<List<UserResponseDto>> findAllUser(){
        return new ResponseEntity<>(userService.findAllUser(), HttpStatus.OK);
    }

    @GetMapping("/users/{userId}")
    public ResponseEntity<UserResponseDto> findUser(@PathVariable Long userId){
        return new ResponseEntity<>(userService.findUserById(userId), HttpStatus.OK);
    }

    @PutMapping("/users/{userId}")
    public ResponseEntity<UserResponseDto> updateUserData(@PathVariable Long userId, @RequestBody UserRequestDto dto){
        return new ResponseEntity<>(userService.updateData(userId, dto), HttpStatus.OK);
    }

    @DeleteMapping("/users/{userId}")
    public ResponseEntity<Void> deleteUserData(@PathVariable Long userId){
        userService.deleteUserData(userId);
        return new ResponseEntity<>(HttpStatus.OK);
    }


}

 

Service

package com.example.scheduledevelop.service;

import com.example.scheduledevelop.dto.*;
import com.example.scheduledevelop.entity.User;
import com.example.scheduledevelop.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;

    @Transactional
    public UserResponseDto signUp(SignUpRequestDto dto){
        User user = new User(dto.getEmail(), dto.getName(), dto.getPassword());
        User saveUser = userRepository.save(user);

        return new UserResponseDto(saveUser.getId(), saveUser.getEmail(), saveUser.getName());
    }

    @Transactional
    public LoginResponseDto login(LoginRequestDto dto){
        User user = userRepository.findByEmail(dto.getEmail()).orElseThrow(()-> new ResponseStatusException(HttpStatus.NOT_FOUND,"해당 이메일의 유저가 존재하지 않습니다"));

        if(!user.getPassword().equals(dto.getPassword())){
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다.");
        }

        return new LoginResponseDto(user.getId(),user.getName(), user.getEmail(), user.getPassword());
    }

    @Transactional(readOnly = true)
    public List<UserResponseDto> findAllUser(){
        List<User> users = userRepository.findAll();

        List<UserResponseDto> dtos = new ArrayList<>();
        for (User user : users) {
            dtos.add(new UserResponseDto(user.getId(), user.getEmail(), user.getName()));
        }
        return dtos;
    }

    @Transactional(readOnly = true)
    public UserResponseDto findUserById(Long userId){
        User user = userRepository.findById(userId).orElseThrow(
                () -> new IllegalArgumentException("해당 유저가 존재하지 않습니다")
        );
        return new UserResponseDto(user.getId(), user.getEmail(), user.getName());
    }

    @Transactional
    public UserResponseDto updateData(Long userId, UserRequestDto dto){
        User user = userRepository.findById(userId).orElseThrow(
                () -> new IllegalArgumentException("해당 유저가 존재하지 않습니다")
        );
        user.update(dto.getUserId(), dto.getEmail(), dto.getName(), dto.getPassword());

        return new UserResponseDto(user.getId(), user.getEmail(), user.getName());
    }

    @Transactional
    public void deleteUserData(Long userId){
        if(!userRepository.existsById(userId)){
            throw  new IllegalArgumentException("해당 유저가 존재하지 않습니다");
        }
        userRepository.deleteById(userId);
    }

}

 

Repository

package com.example.scheduledevelop.repository;

import com.example.scheduledevelop.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

 

Lv3 회원가입

 

Controller

@PostMapping("/users/signup")
public ResponseEntity<UserResponseDto> signup(@RequestBody SignUpRequestDto signUpRequestDto){
    UserResponseDto userResponseDto = userService.signUp(signUpRequestDto);
    return new ResponseEntity<>(userResponseDto, HttpStatus.CREATED);
}

- 유저 컨트롤러에서 유저서비스에 메소드를 만들어 작동

 

Service

@Transactional
public UserResponseDto signUp(SignUpRequestDto dto){
    User user = new User(dto.getEmail(), dto.getName(), dto.getPassword());
    User saveUser = userRepository.save(user);

    return new UserResponseDto(saveUser.getId(), saveUser.getEmail(), saveUser.getName());
}

 

Dto

package com.example.scheduledevelop.dto;

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

@Getter
@NoArgsConstructor
public class SignUpRequestDto {
    @NotBlank(message = "이름을 입력해주세요")
    private String name;

    @NotBlank(message = "이메일을 입력해주세요")
    private String email;

    @NotBlank(message = "비밀번호를 입력해주세요")
    private String password;

    public SignUpRequestDto(String email, String password) {
        this.email = email;
        this.password = password;
    }
}

- dto로 요청 받은 데이터를 유저레포지토리를 통해 유저에 저장해 유저 리스폰스를 통해 출력

 

Lv4 로그인 인증

로그인 인증을 위해 필러를 등록

package com.example.scheduledevelop.config;

import com.example.scheduledevelop.filter.LoginFilter;
import jakarta.servlet.Filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public FilterRegistrationBean loginFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LoginFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");

        return filterFilterRegistrationBean;
    }

}

 

Filter

private static final String[] WHITE_LIST = {"/", "/users/signup", "/login", "/logout"};

 

해당 URL은 로그인 검증을 하지 않음

//화이트 리스트에 포함된 경우 true > !ture > false
    if (!isWhiteList(requestURI)) {

        HttpSession session = httpRequest.getSession(false);
        if (session == null || session.getAttribute("userId") == null){
            throw new RuntimeException("로그인 해주세요");
        }

        log.info("로그인에 성공하였습니다");

    }

    // 1번 경우 :  화이트 리스트에 등록된 유알엘이면 바로 체인두필터가 호출
    // 2번 경우 : 화이트 리스트가 아닌 경우 위의 필터 로직을 통과후 체인두필터로 다음 필터나 서블릿을 호출
    // 다음 필터가 없으면 서블릿이나 컨드롤러, 다음 필터가 있으면 다음 필터를 호출
    filterChain.doFilter(servletRequest, servletResponse);

}
private boolean isWhiteList(String requestURI) {
    return PatternMatchUtils.simpleMatch(WHITE_LIST, requestURI);
}

-화이트 리스트 검증 로직

 

Controller

package com.example.scheduledevelop.controller;

import com.example.scheduledevelop.dto.LoginRequestDto;
import com.example.scheduledevelop.dto.LoginResponseDto;
import com.example.scheduledevelop.dto.SignUpRequestDto;
import com.example.scheduledevelop.dto.UserResponseDto;
import com.example.scheduledevelop.entity.User;
import com.example.scheduledevelop.service.UserService;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequiredArgsConstructor
public class LoginController {

    private final UserService userService;

    @PostMapping("/login")
    public ResponseEntity<LoginResponseDto> login(@RequestBody LoginRequestDto dto, HttpServletRequest request){
        LoginResponseDto loginResponseDto = userService.login(dto);

        HttpSession session = request.getSession(false); // 없을시 null 반환(실패한 적이 있다면 세션이 있을수도)
        if (session == null) {
            session = request.getSession(true);  // 없을경우 있게 만들어줌
        }


        if (loginResponseDto != null) {
            log.info("로그인 성공 유저(userId, name, email) = {}, {}, {}",
                    loginResponseDto.getUserId(),
                    loginResponseDto.getName(),
                    loginResponseDto.getEmail());
            session.setAttribute("userId", loginResponseDto.getUserId());
            session.setMaxInactiveInterval(1800); //30분
        }
        return new ResponseEntity<>(loginResponseDto, HttpStatus.OK);
    }

    @PutMapping("/logout")
    private ResponseEntity<Void> logout(HttpServletRequest request){
        HttpSession session = request.getSession(false);
        if (session != null){
            session.invalidate();
        }
        return new ResponseEntity<>(HttpStatus.OK);
    }

}

 

Service

@Transactional
public LoginResponseDto login(LoginRequestDto dto){
    User user = userRepository.findByEmail(dto.getEmail()).orElseThrow(()-> new ResponseStatusException(HttpStatus.NOT_FOUND,"해당 이메일의 유저가 존재하지 않습니다"));

    if(!user.getPassword().equals(dto.getPassword())){
        throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다.");
    }

    return new LoginResponseDto(user.getId(),user.getName(), user.getEmail(), user.getPassword());
}

- 유저 서비스에 로그인 로직 작성

 

[Postman 실행]

회원가입

 

 

- 회원 가입

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


- 로그인 성공

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


- 로그인 실패(401에러 발생)

 

 

 

 

 

 

 

 

 

 

 

 

 


 

-유저 조회

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

-전제 유저 조회

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

- 유저 정보 수정

(작성 안한부분은 원래대로 업데이트)

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

- 회원 삭제시 이메일과 비밀번호 입력

 

 

 

 

 

 

 

 

 

 

 

 

 


 

- 일정 생성

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

- 전체 일정 조회

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


- 일정 단일 조회

   (유저아이디로)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

- 일정 수정

(제목이나 내용이 전달이 안되면 기존 데이터로 업데이트)

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

- 일정 삭제

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


<느낀점>

전반적으로 오류나 힘든 점은 없었으나 세션을 주고 받는것을 이해하는데 오래걸렸다.

'내일배움캠프 > TIL' 카테고리의 다른 글

[Spring]_JWT Refresh token  (0) 2025.03.07
[Spring]_뉴스피드 팀프로젝트 트러블 슈팅  (0) 2025.02.20
[Spring]_일정관리 트러블슈팅  (0) 2025.02.04
[Spring]_MVC  (0) 2025.01.23
[Spring]_ Framework  (0) 2025.01.23