저번주 월요일에 일정관리 앱을 만드는 과제를 받았다. 과제에 집중하느라 블로그 작성이 소홀해졌다...ㅠ
이번 과제에서 구현해야할 기능들은 아래와 같다.
- 일정 생성 및 조회
- 일정 수정
- 일정 삭제
- 일정 테이블과 작성자 테이블의 연관관계 설정
- 페이지네이션
- API 명세서 작성
- ERD 작성
이렇게 크게 5가지이다.거의 처음하다시피하는 스프링으로 하다보니 생성 조회부터 막혔었다....그래도 과제 제출일인 오늘까지 꾸역꾸역 다 구현해서 다행인것같다.
API 명세서와 erd는 시작하기 전에 작성해보고 개발을 했지만 API 명세서 같은 경우는 굉장히
많이 추가됐다.
개발 전 쓴 API 명세서
처음 작성하는것이다보니 그냥 예시에 나와있는 것과 굉장히 유사하게 작성했었다..
솔직히 이정도면 굴러가겠구나 싶기도했지만 크나큰 착각이였다
모두 마친 후 API 명세서
다 끝마친 후에는 포스트맨을 이용하여 API 명세서를 만들어봤다.페이지네이션을 하기 위해 전체 행을 뽑아오는 코드들이 추가되면서 api 명세서에도 추가해야할 것들이 많아졌다.
https://documenter.getpostman.com/view/21774594/2sAXxLCuf3
이번 프로젝트의 전체적인 흐름은 클라이언트의 요청이 들어오면 ajax를 이용하여 Controller으로 요청을 보내고
Controller -> service -> repository -> serivce -> Controller -> 응답의 흐름으로 개발했다.
repository에서는 Db에서 값을 꺼내거나 저장시키는 역할을 담당하였고,
service 단에서 requestDTO 객체를 entity 객체로 변환해서 repository에 보내 값을 받아 온 후,
다시 responseDTO 객체로 변환하여 사용자에게 요청에 대한 응답을 보냈다.
이제 생성부터 시작해보자
일정 생성
클라이언트가 일정 등록버튼을 누르면 ajax를 통해 유효성 검사 후
받은 이메일로 User 테이블의 그 이메일이 존재하는지 확인 한 후 없다면, user테이블에 작성자를 생성해 준 뒤,
그 user_id를 가져와서 Controller로 일정 생성 요청을 보낼 때 같이 보낸다.
만약 같은 이메일이 user 테이블에 존재한다면 그 userd의 user_id를 가져와서
Controller로 일정 생성 요청을 보낼 때 같이 보낸다.
function writeSchedule(){
let username = $('#username').val();
let user_email = $('#user_email').val();
let password = $('#password').val();
let title = $('#title').val();
let content = $('#content').val();
let date = $('#date').val();
let start_date = date.substring(0,10);
let end_date = date.substring(13);
if(!inputCheck(username,password,title,content)) return;
if(emailChecked(user_email) === 0){
alert("이메일을 입력해주세요");
return;
}
if(emailChecked(user_email) === 1){
alert("이메일 형식에 맞게 입력해주세요");
return;
}
let data = {
'user_email':user_email
};
$.ajax({
// user테이블의 작성자에게 입력받은 이메일이 존재하는지 확인한다.
type : 'POST',
url : "/user/checking",
dataType : 'json',
data : data,
success : function (response){
let user_id = response.user_id;
// 넘겨받은 id가 0이 아니라는 것은
// user 테이블에 같은 이메일이 존재한다는 것이므로
// controller에 보낼 때 이 user_id도 같이 보낸다.
if(user_id !== 0){
let data = {
'user_id' : user_id,
'username': username,
'password': password,
'title' : title,
'content': content,
'start_date' : start_date,
'end_date' : end_date
};
$.ajax({
type : 'POST',
url : '/scheduler',
contentType : 'application/json',
data : JSON.stringify(data),
success : function(response){
alert('일정 등록 완료 ');
location.href = '/';
}
});
}
// 넘겨받은 id값이 0이라면 존재하지 않는다는 것이기 때문에
// 먼저 user테이블에 email과 username을 저장시키고 그 저장한
// user_id를 반환 받아서
// controller에 같이 넘겨준다.
else{
let data = {
'user_email': user_email,
'username': username
};
$.ajax({
type: 'POST',
url: '/user/create',
data: JSON.stringify(data),
contentType : 'application/json',
dataType: 'json',
success: function (response) {
let user_id = response.user_id;
let data = {
'user_id': user_id,
'username': username,
'password': password,
'title': title,
'content': content,
'start_date': start_date,
'end_date': end_date
};
$.ajax({
type: 'POST',
url: '/scheduler',
contentType: 'application/json',
data: JSON.stringify(data),
success: function (response) {
alert('일정 등록 완료 ');
location.href = '/';
}
});
}
});
}
}
});
}
function inputCheck(name, password, title, content) {
if (name == '') {
alert("이름을 입력해주세요");
return false;
}
if (password == '') {
alert("비밀번호를 입력해주세요");
return false;
}
if (title == '') {
alert("제목을 입력해주세요");
return false;
}
if (content == '') {
alert("할일을 입력해주세요");
return false;
}
return true;
}
일정 조회
일정 조회는 전체 조회, 작성자 및 수정일로 검색 조회,
사용자의 이메일을 입력받아 사용자가 쓴 게시글만 보여주는 조회
크게 이렇게 3개로 나뉘지만, 너무 길어질것같아서 전체조회만 쓰려고한다.
이 3개의 조회 모두 페이징 처리를 위해 조회하기전에 먼저 행의 수를 구하는 동작부터 수행한다.
먼저 페이지네이션을 위해 만든 페이지 클래스부터 보자.
페이지네이션은 다른 분들의 블로그를 참고하여 만들었다.
package com.sparta.scheduler.page;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class Page {
/* 현재 페이지 */
private int pageNum;
// 전체 행의 수
private int total;
// 전체 페이지 수
private int totalPages;
// 시작 페이지 번호
private int startPage;
// 마지막 페이지 번호
private int endPage;
// 페이징의 개수
private int pagingCount;
// size : 한 화면에 보여질 행의 수
public Page(int total, int pageNum, int size, int pagingCount) {
this.total = total;
this.pageNum = pageNum;
this.pagingCount = pagingCount;
// select 결과가 없다면 모두 0으로 초기화
if(total == 0){
totalPages = 0;
startPage = 0;
endPage = 0;
}
// select 결과가 있다면
// 전체 페이지 수 구하기
// 정수와 정수의 나눗셈의 결과는 정수이므로
else{
totalPages = total / size;
// 나머지가 0보다 클 경우 전체 페이지 수를 1증가
if(total % size > 0){
totalPages++;
}
// startPage : 'prev [1] [2] [3] [4] [5]... next' 일때 1을 의미
// 현재 페이지 / 페이징의 개수 * 페이징의 개수 + 1;
startPage = pageNum / pagingCount * pagingCount + 1;
// 현재 페이지 % 10 == 0 일때
if(pageNum % pagingCount == 0){
// startPage = startPage - 10;
startPage -= pagingCount;
}
// endpage : 'prev [1] [2] [3] [4] [5]... next' 일때 5를 의미
endPage = startPage + pagingCount - 1 ;
// endPage > totalPages일때는 전체 페이지의 개수를 넘은것 이기에
// endPage에 전체 페이지수를 대입
if(endPage > totalPages){
endPage = totalPages;
}
}
}
}
function getPaging(num,total){
// 맨 처음 호출 할때 num은 1, total은 0이 넘어가도록 설정해두었다.
let pageNum = {
'pageNum': num,
'total' : total
};
$.ajax({
type : 'GET',
url : '/scheduler/page',
dataType : 'json',
data: pageNum,
success : function (response){
// 페이징 처리를 하기 위하여 만들어뒀던 Page 객체를 받아
// 버튼들을 생성하고 난 뒤,
// pageNum을 보내주며 게시글을 불러온다.
$('#paginationUl').empty();
console.log(response.total)
let startPage = response.startPage;
let pageNum = response.pageNum;
let totalPages = response.totalPages;
let endPage = response.endPage;
addBtnHtml(startPage,pageNum,totalPages,endPage);
getSchedules(pageNum);
}
});
}
// pageNum을 같이 보내주는 이유는
// pageNum의 값으로 repository단에서 limit을 이용하여 정보들을 뽑아올 것 이기
// 때문이다.
// 만약 pageNum이 2라는 것은 2페이지의 정보를 불러오라는 것이기에
// service단에서 2 *= 10을하여 20을 만들고
// 그 20의 값을 레퍼지토리에 넘기며 정보를 받아올 때 limit 20, 10으로 받아온다.
// 여기서 뒤의 20, 10은 20행부터 10개만 가져오라는 뜻이다.
function getSchedules(num) {
$('#tbody-box').empty();
let data = {'pageNum': num};
$.ajax({
type: 'GET',
url: '/scheduler/paging',
dataType: 'json',
data: data,
success: function (response) {
for (let i = 0; i < response.length; i++) {
let schedules = response[i];
let schedule_id = schedules['schedule_id'];
let username = schedules['username'];
let title = schedules['title'];
let modification_date = schedules['modification_date'];
indexAndMyScheduleViewAddHTML(schedule_id, username, title, modification_date);
}
}
})
}
일정 수정
먼저 비밀번호를 입력받고 비밀번호와 게시글의 schedule_id을 가져가서 유무 확인 후
비밀번호가 맞다면 수정 폼을 보여주고 아니라면 비밀번호가 틀리다는 alert창을 띄워준다.
이후 사용자가 수정확인 버튼을 클릭하면 수정을 해주고 취소 클릭한다면 게시글 뷰로 돌려보낸다.
function checking(){
let password = $('#password').val();
let param = `schedule_id=${schedule_id}&password=`+password;
if(password === ''){
alert("비밀번호를 입력해주세요.");
return;
}
$.ajax({
type : 'POST',
url : '/passwordCheck',
data : param,
dataType : 'json',
success : function(data){
// 비밀번호 입력 인풋을 hidden style 부여된 hidden 클래스를 추가해준다.
$('#input-content').attr('class' ,"hidden");
// 감춰뒀던 input을 보여주기위해 removeAttr로 hidden 클래스 요소를 지워준다.
$('#appendContainer').removeAttr('class');
},
error : function(){
alert("비밀번호를 확인해주세요");
}
});
}
function modifySchedule(){
let username = $('#username').val();
let content = $('#content').val();
let title = $('#title').val();
let date = $('#date').val();
let start_date = date.substring(0,10);
let end_date = date.substring(13);
let data = {
'username':username,
'title':title,
'content':content ,
'start_date':start_date,
'end_date':end_date
};
if(username === ''){
alert("이름을 제대로 입력해주세요");
return;
}
if(content === ''){
alert("일정을 제대로 입력해주세요");
return;
}
$.ajax({
type : 'PUT',
url : `/scheduler/${schedule_id}`,
contentType : 'application/json',
data : JSON.stringify(data),
success : function(){
alert('수정 완료!');
$('#form').submit();
},
error : function (){
alert("업데이트 오류 발생");
}
});
}
일정 삭제
일정 수정과 마찬가지로 비밀번호를 먼저 입력받고 비밀번호가 일치한지 확인 후 일치하다면 삭제해준다.
function deleteSchedule(){
let schedule_id = $('#schedule_id').val();
let password = $('#password').val();
let param = `schedule_id=${schedule_id}&password=`+password;
if(password === ''){
alert("비밀번호를 입력해주세요");
return;
}
if(confirm("게시글을 정말 삭제하시겠습니까 ?")){
$.ajax({
type : 'POST',
url : '/passwordCheck',
dataType : 'json',
contentType : 'application/x-www-form-urlencoded',
data : param,
success : function(data){
$.ajax({
type : 'DELETE',
url : `/scheduler/${schedule_id}`,
success : function(){
alert("삭제 완료 !");
location.href = "/";
}
});
},
error : function(data,error){
alert('비밀번호를 확인해주세요');
}
});
}
}
SchedulerRepository
DB와 직접적인 소통을 하는 클래스이다.
@Repository
public class SchedulerRepository {
private final JdbcTemplate jdbcTemplate;
public SchedulerRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// 일정 추가
public Scheduler insert(Scheduler scheduler) {
KeyHolder keyHolder = new GeneratedKeyHolder();
String sql = "INSERT INTO schedules(user_id,username,password,title,content,start_date,end_date) VALUES (?,?,?,?,?,?,?)";
jdbcTemplate.update(con -> {
PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
ps.setLong(1, scheduler.getUser_id());
ps.setString(2, scheduler.getUsername());
ps.setString(3, scheduler.getPassword());
ps.setString(4, scheduler.getTitle());
ps.setString(5, scheduler.getContent());
ps.setString(6, scheduler.getStart_date());
ps.setString(7, scheduler.getEnd_date());
return ps;
},keyHolder);
scheduler.setSchedule_id(keyHolder.getKey().longValue());
return scheduler;
}
// 전체 일정 조회
public List<Scheduler> selectAll() {
String sql = "SELECT * FROM schedules order by schedule_id desc";
return jdbcTemplate.query(sql, new RowMapper<Scheduler>() {
public Scheduler mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Scheduler(rs);
}
});
}
// 일정 아이디로 한가지의 게시글만 조회
public Scheduler findById(Long schedule_id) {
String sql = "SELECT * FROM schedules WHERE schedule_id=?";
return jdbcTemplate.query(sql, resultSet->{
if(resultSet.next()) {
return new Scheduler(resultSet);
}
else{
return null;
}
}, schedule_id);
}
// 일정 삭제
public Long delete(Long schedule_id) {
String query = "DELETE FROM schedules WHERE schedule_id = ?";
jdbcTemplate.update(query, schedule_id);
return schedule_id;
}
// password 확인
public Long checkPassword(Long schedule_id, String password){
Connection connection;
PreparedStatement ps;
String query = "SELECT * FROM schedules WHERE schedule_id = ? AND password = ?";
try {
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/scheduler","root","dksdkffiwna0~!");
ps = connection.prepareStatement(query);
ps.setLong(1, schedule_id);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();
if(rs.next()) {
return rs.getLong("schedule_id");
}else{
return null;
}
} catch (SQLException e) {
throw new RuntimeException("checkPassword - 런타임 에러");
}
}
// 일정 수정
public Long update(Long schedule_id, Scheduler scheduler) {
String query = "update schedules set username = ?,title = ?, content = ?, start_date = ?, end_date = ? where schedule_id = ?";
jdbcTemplate.update(query, scheduler.getUsername(),scheduler.getTitle(), scheduler.getContent(), scheduler.getStart_date(), scheduler.getEnd_date(),schedule_id);
return schedule_id;
}
// 수정일로만 검색했을 때
public List<Scheduler> selectSearchModifyDate(String date, int pageNum) {
String query = "select * from schedules where DATE_FORMAT(modification_date,'%Y-%m-%d') = ? order by modification_date desc limit ? , 10";
return jdbcTemplate.query(query,new RowMapper<Scheduler>(){
@Override
public Scheduler mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Scheduler(rs);
}
},date,pageNum);
}
// 수정일로만 검색했을 때 나오는 결과값의 토탈 수를 구하기 위한 메서드
public List<Scheduler> selectSearchModifyDateLength(String date) {
String query = "select * from schedules where DATE_FORMAT(modification_date,'%Y-%m-%d') = ? order by modification_date desc";
return jdbcTemplate.query(query,new RowMapper<Scheduler>(){
@Override
public Scheduler mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Scheduler(rs);
}
},date);
}
// 작성자명으로만 검색했을 때
public List<Scheduler> selectSearchAuthor(String username, int pageNum) {
String query = "select * from schedules where username = ? order by modification_date desc limit ? , 10";
return jdbcTemplate.query(query,new RowMapper<Scheduler>(){
@Override
public Scheduler mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Scheduler(rs);
}
},username,pageNum);
}
// 작성자명으로만 검색했을 때 결과값의 토탈 수를 구하기위한 메서드
public List<Scheduler> selectSearchAuthorLength(String username) {
String query = "select * from schedules where username = ? order by modification_date desc";
return jdbcTemplate.query(query,new RowMapper<Scheduler>(){
@Override
public Scheduler mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Scheduler(rs);
}
},username);
}
// 수정일, 작성자명 둘 다 검색
public List<Scheduler> selectSearchAll(String username, String date, int pageNum) {
String query = "select * from schedules where username = ? and DATE_FORMAT(modification_date,'%Y-%m-%d') = ? order by modification_date desc limit ?,?";
return jdbcTemplate.query(query,new RowMapper<Scheduler>(){
@Override
public Scheduler mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Scheduler(rs);
}
},username,date,pageNum,10);
}
// 수정일, 작성자명 둘 다 검색했을 때 결과값의 토탈 수를 구하는 메서드
public List<Scheduler> selectSearchAllLength(String username, String date) {
String query = "select * from schedules where username = ? and DATE_FORMAT(modification_date,'%Y-%m-%d') = ? order by modification_date desc";
return jdbcTemplate.query(query, new RowMapper<Scheduler>() {
@Override
public Scheduler mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Scheduler(rs);
}
}, username, date);
}
// 전체 글을 페이징처리하며 뽑아낸다.
public List<Scheduler> selectAllByPaging(int pageNum) {
String query = "select * from schedules order by modification_date desc limit ?,? ";
return jdbcTemplate.query(query, new RowMapper<Scheduler>(){
public Scheduler mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Scheduler(rs);
}
},pageNum,10);
}
// 내가 작성한 글 리스트들을 반환
public List<Scheduler> selectByMySchedule(Long user_id, int pageNum) {
String query = "select * from schedules where user_id = ? order by modification_date desc limit ?,10";
return jdbcTemplate.query(query, new RowMapper<Scheduler>(){
@Override
public Scheduler mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Scheduler(rs);
}
}, user_id,pageNum);
}
// 내가 작성한 글 리스트들의 토탈 수를 반환해주기 위한 메서드
public List<Scheduler> selectByMyScheduleCnt(Long user_id) {
String query = "select * from schedules where user_id = ? order by modification_date desc";
return jdbcTemplate.query(query, new RowMapper<Scheduler>(){
@Override
public Scheduler mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Scheduler(rs);
}
}, user_id);
}
}
게시판 메인 화면 이미지
Controller과 Service까지 넣기에는 너무 길어질 것 같아서 깃헙 주소를 남긴다.https://github.com/HaSeung2/schedules
회고
이번 과제를 진행하며 수많은 트러블이 있었지만 모두 구글링을 통하여 해결은 하였지만,
과제 기능을 다 구현해보기 위해 집중하느라 트러블을 바로바로 정리하지 못 한것이 가장 아쉽다.
트러블 슈팅이 습관화되면 나중에도 내가 뭘 몰랐는지, 어떻게 해결하였는지를 내가 쓴 트러블 슈팅을 보고
복기할 수 있었을텐데 이번엔 거의 처음 접해보는 것들을 많이 써보고 그것들을 이용하여 과제를 진행해야 했기에
트러블 슈팅을 제쳐두게되었는데, 다음부터는 글을 쓰진 못해도 뭘 몰랐는지, 어떤 문제가 발생하였는지
메모라도 해봐야겠다. 그렇게 메모하는 습관이 들다보면 자연스레 트러블 슈팅이 습관화 되지않을까 기대해본다.
'프로젝트' 카테고리의 다른 글
내배캠 - 최종 프로젝트 SpotOn (0) | 2025.01.08 |
---|---|
내배캠 - 플러스 팀 프로젝트 (1) | 2024.12.02 |
아웃소싱 프로젝트 - 회고 (4) | 2024.11.07 |
뉴스피드 프로젝트 (0) | 2024.10.24 |