⏰커벨리오(Curvelio) : 시간표 생성 프로그램

­Valentine·2021년 5월 18일
0

Project

목록 보기
5/7


안녕하세요! 오늘은 졸업프로젝트로 진행한 대학생 시간표 생성 프로그램, 커벨리오를 만들면서 제가 참여한 부분과 그 과정에서 느낀점등을 정리해보고자 합니다 :)

1. Java(백)☕

#MySQL연동

대학생을 대상으로 하는 시간표 생성 프로그램의 특성상 방대한 양의 데이터가 필요해서 DB를 사용하는 것이 필수적이었습니다. 그래서 저희는 관계형 데이터베이스중 많이 사용되는 MySQL을 연동하여 사용하기로 하였습니다.

  1. 환경

    • java : version 8
    • eclipse : 2020-6
    • mysql : 8.0.19 for Win64 on x86_64
  2. JDK 다운로드

  3. JDBC 파일 추가

  4. java 프로젝트 생성

    • eclipse에서 java 프로젝트를 생성
    • Create module-info.java file를 취소
  5. 프로젝트 설정

    • JDBC파일을 추가할 폴더 lib을 생성
    • jar파일을 lib에 복사
    • 프로젝트에서 build path→configure build path를 선택
    • java build path→libraries→Classpath→Add JARs를 클릭하여 lib밑에 추가해두었던 jar파일을 선택→apply and close
  6. 참고

    MySQLJDBC연동.pdf

#시간표 생성 알고리즘 프로토타입

프로젝트가 진행되면서 java 코드는 다른 팀원이 전담하게 되어서 제가 작성한 부분에서 디버깅을 어떻게 하였는지를 중심으로 작성해보았습니다..!

1. 다수의 테이블에서 정보를 불러올 때

Connection conn = null;
Statement state = null;
Statement state3 = null;
ResultSet resset = null;
ResultSet resset2 = null;
ResultSet resset3 = null;
PreparedStatement pstmt = null;
PreparedStatement pstmt2 = null;
PreparedStatement pstmt3 = null;
PreparedStatement pstmt4 = null;
//중략
sql = "select * from majors where recommend_time=32";
sql2 = "insert into graduate.time_table (table_number,week,period,course_name,division_number,professor_name) values(?,?,?,?,?,?)";
sql3 = "select * from user";
sql4 = "select * from user1_dropmajor";
pstmt = conn.prepareStatement(sql);
pstmt2 = conn.prepareStatement(sql2);
pstmt3 = conn.prepareStatement(sql3);
pstmt4 = conn.prepareStatement(sql4);
String major_name = "";
String this_time = "";
String professor_name = "";
resset = state.executeQuery(sql);
while (resset.next()) {
	major_name = resset.getString("major_name");
	table_name.add(major_name);
	this_time = resset.getString("this_time");
	table_time.add(this_time);
	professor_name = resset.getString("professor_name");
	table_professor.add(professor_name);
}resset.close();// close resultset
resset = state.executeQuery(sql3);
String Timeout = "";
while (resset.next()) {
	Timeout = resset.getString("time_out");
	out_time.add(Timeout);
}resset.close();// close resultset
resset = state.executeQuery(sql4);
String out_name = "";
String out_div = "";
String out_flag = "";
while (resset.next()) {
	out_name = resset.getString("major_name");
	name_out.add(out_name);
	out_div = resset.getString("division_number");
	div_out.add(out_div);
	out_flag = resset.getString("flag");
	flag_out.add(out_flag);
}
  • mysql로 하나의 테이블을 불러와서 사용하는 것만 알고 있었는데 다수의 테이블을 사용해야했습니다.
  • 위와 같이 선언을 각각 해주고 select(검색) 쿼리는 executeQuery로, insert(입력)쿼리는 executeUpdate로 각각 실행시켜주었습니다.
  • query를 수행한 후에 ResultSet을 닫고 다시 여는 식으로 해결하였습니다!

2. 분반 확인

for (int m = 0; m < table_name.size(); m++) { // ArrayList 만큼 반복
	if(table_name.get(m)!=null) {
		if (duplicate_count.containsKey(table_name.get(m))){ // HashMap 내부에 이미 key 값이 존재하는지 확인
			duplicate_count.put(table_name.get(m), duplicate_count.get(table_name.get(m)) + 1); // key가 이미 있다면 value에 +1
		} else { // key값이 존재하지 않으면
			duplicate_count.put(table_name.get(m), 1); // key 값을 생성후 value를 1로 초기화
		}
	}
}
  • 같은 과목인데 다른 분반인 과목이 시간표에 동시에 들어가면 안되기 때문에 한 과목당 분반이 몇개 있는지 파악하는 코드를 생성했습니다.

3.시간표 생성 함수(table_maker)

public static int table_maker(String[] semi_table, int table2_number) {
	int flag=0;
	for (int i = 0; i < 6; i++) {
		if (semi_table[i].length() == 2) {
		day1 = (Integer.parseInt(semi_table[i].substring(0, 1)) - 1);
		time1 = (Integer.parseInt(semi_table[i].substring(1, 2)) - 1);
		for(int j=0;j<out_time.get(1).length();j++) {
			if(j%2==0) {
 				if(day_out[j]==(day1+1)&&time_out[j+1]==(time1+1)) {
					flag=1;
                    		}
			}
		}
	real_table[day1][time1][table2_number] = Integer.toString(i);
	} else if (semi_table[i].length() == 4) {
				day1 = (Integer.parseInt(semi_table[i].substring(0, 1)) - 1);
				time1 = (Integer.parseInt(semi_table[i].substring(1, 2)) - 1);
				day2 = (Integer.parseInt(semi_table[i].substring(2, 3)) - 1);
				time2 = (Integer.parseInt(semi_table[i].substring(3, 4)) - 1);
				for(int j=0;j<out_time.get(1).length();j++) {
					if(j%2==0) {
						if(day_out[j]==(day1+1)&&time_out[j+1]==(time1+1)) {
							//System.out.println(day1+" "+time1);
							flag=1;
						}if(day_out[j]==(day2+1)&&time_out[j+1]==(time2+1)) {
							//System.out.println(day2+" "+time2);
							flag=1;
						}
					}
				}
				real_table[day1][time1][table2_number] = Integer.toString(i);
				real_table[day2][time2][table2_number] = Integer.toString(i);
			} else if (semi_table[i].length() == 6) {
				day1 = (Integer.parseInt(semi_table[i].substring(0, 1)) - 1);
				time1 = (Integer.parseInt(semi_table[i].substring(1, 2)) - 1);
				day2 = (Integer.parseInt(semi_table[i].substring(2, 3)) - 1);
				time2 = (Integer.parseInt(semi_table[i].substring(3, 4)) - 1);
				day3 = (Integer.parseInt(semi_table[i].substring(4, 5)) - 1);
				time3 = (Integer.parseInt(semi_table[i].substring(5, 6)) - 1);
				for(int j=0;j<out_time.get(1).length();j++) {
					if(j%2==0) {
				if(day_out[j]==(day1+1)&&time_out[j+1]==(time1+1)) {
				flag=1;
				}if(day_out[j]==(day2+1)&&time_out[j+1]==(time2+1)) {
				flag=1;
				}if(day_out[j]==(day3+1)&&time_out[j+1]==(time3+1)) {
				flag=1;
				}
			}
		}
		real_table[day1][time1][table2_number] = Integer.toString(i);
		real_table[day2][time2][table2_number] = Integer.toString(i);
		real_table[day3][time3][table2_number] = Integer.toString(i);
		}
	}return flag;
}
  • 과목당 요일,교시를 2345→화3목5 와 같은 방식으로 입력받았기 때문에 parsing
  • 불가능한 시간이랑 겹치는지 체크하고 int형 flag로 return
  • 생성된 시간표는 전역변수인 realtable[][][]에 들어감
  • 불가능한 시간이랑 겹치지 않으면, 즉 flag가 0이면 해당되는 과목 정보를 해당 시간에 넣음 (ex: real_table[2][3][1]=4이면 1번 시간표에 화요일 3교시는 4번과목(컴퓨터네트워크)임)

4. 불가능한 과목 체크 함수(blank_check)

public static int blank_check(int index) {
	int flag=0;
	for (int i = 0; i < 5; i++) {
		for (int j = 0; j < 7; j++) {
			if (real_table[i][j][index] != null) {
				int n = Integer.parseInt(real_table[i][j][index]);
				if(name_table[index][n]==null) {
					flag=1;
					break;
				}
			}
		}
	}
	return flag;
}
  • 주5일 7교시의 시간표를 생성할건데 이미 과목이 채워져있는 곳에는 과목을 새로 입력할 수 없으므로 확인이 필요합니다.
  • 불가능한 과목은 이름을 null로 변경하여 불가능한 과목이 포함되면 int flag를 1로 return

5. 시간표 db에 저장하기

			String day = "";
			String course_name = "";
			String prof_name = "";
			f=table_maker(created_table[0], 0);
			int flag2=blank_check(0);
			/*
			System.out.println(f);
			System.out.println(flag2);*/
			if (f==0 && flag2==0) {
				for (int i = 0; i < 5; i++) {
					for (j = 0; j < 7; j++) {
						if (real_table[i][j][0] != null) {
							if (i == 0) {
								day = "월";
							} else if (i == 1) {
								day = "화";
							} else if (i == 2) {
								day = "수";
							} else if (i == 3) {
								day = "목";
							} else if (i == 4) {
								day = "금";
							}
							int bun = 0;
							int n = Integer.parseInt(real_table[i][j][0]);
							course_name = name_table[0][n];
							bun = bunban_table[0][n];
							prof_name = pname_table[0][n];
							pstmt2.setInt(1, 1);
							pstmt2.setString(2, day);
							pstmt2.setInt(3, j + 1);
							pstmt2.setString(4, course_name);
							pstmt2.setInt(5, bun);
							pstmt2.setString(6, prof_name);
							int r = pstmt2.executeUpdate();
						}
					}
				}
			}
  • flag 두개 다 통과되면 데베에 넣었습니다.
  • Java 코드로 사용자가 해당 학기에 들어야 하는 과목을 불러온 후, 사용자가 선택한 듣지 않을 과목에 대한 정보와 불가능한 시간대에 관한 정보를 불러와 해당하는 과목을 제외시키는 기능을 구현하였습니다.
  • 그 후, 가능한 시간표를 모두 생성하여 시간표에 생성 함수에 넣고 결과를 DB에 저장하는 부분까지 구현에 성공하고 저는 일반 java 코드를 spring으로 바꾸는 작업에 착수했습니다.

2. Spring(백)🌱

#환경설정

IDE는 IntelliJ를 사용하였습니다

1. 강의

spring framework를 사용해보는 것이 처음이라 강의를 들으면서 시작해보았습니다.

2. 다운로드

강의에 따라 다운로드를 받기 위해 Spring initializer에 접속했습니다. 언어는 java를 사용하는 것을 선택하고 Maven과 gradle중에 강의를 따라 gradle을 선택하였는데 자료는 maven이 더 많은것 같아서 취향에 따라 선택하시면 될 것 같습니다.
java의 버전은 더 높은 버전만 아니면 일단 낮은 버전을 다운받고 gradle설정에서 변경하였습니다. Dependency는 기본적인 Spring Web과 Thymeleaf, java를 사용할 것이기 때문에 JPA도 추가해주었습니다.

3. Gradle 설정

plugins {
	id 'org.springframework.boot' version '2.3.4.RELEASE'
	id 'io.spring.dependency-management' version '1.0.10.RELEASE'
	id 'java'
}

group = 'com.board'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.projectlombok:lombok'
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'mysql:mysql-connector-java'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

test {
	useJUnitPlatform()
}
  • build.gradle 파일에 들어가서 gradle 설정이 필요할때마다 수정해주면 됩니다.

#실행

  • /src/main/java에 들어가면 폴더명+Application.java가 자동 생성 되어있고 초록색 화살표를 누르면 빌드+실행이 가능합니다.

  • 오류없이 성공적으로 실행되었다면 http://localhost:8080/에 접속하여 다음과 같은 화면을 보실 수 있을겁니다.

#개요

spring에서 JPA를 사용한다면 코드는 크게 Controller, Model, Repository, Service의 4가지 부분으로 이루어집니다. 이름은 개발자마다 다를 수 있지만 기본적인 데이터의 틀을 선언하여 DB와 직접 연동되는 Model, 이 모델을 활용하여 DB에 쿼리를 날리고 Service에 그 값을 반환하는 Repository, Controller에서 받은 parameter 값이나 Repository에서 받은 return 값을 이용하여 실질적인 기능을 구현하는 Service, 마지막으로 프론트와 Spring을 연동해주며 Service의 함수들을 호출하는 Controller의 기본적인 틀은 일치할 것입니다.

#Model

package com.board.back.model;

import lombok.NoArgsConstructor;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

//유저테이블
@Entity
@Table(name = "User")
@DynamicInsert
@DynamicUpdate
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_no")
    private Integer user_no;
    // 아이디(학번)
    @Column(name = "user_id")
    private Integer user_id;

    // 비밀번호
    @Column(name = "user_pw")
    private String user_pw;

    // 전공
    @Column(name = "user_major")
    private Integer user_major;

    // 학년
    @Column(name = "user_grade")
    private Integer user_grade;

    public Integer getNo() {
        return user_no;
    }

    public void setNo(Integer user_no) {
        this.user_no = user_no;
    }

    public Integer getId() {
        return user_id;
    }

    public void setId(Integer user_id) {
        this.user_id = user_id;
    }

    public String getPassword() {
        return user_pw;
    }

    public void setPassword(String user_pw) {
        this.user_pw = user_pw;
    }

    public Integer getMajor() {
        return user_major;
    }

    public void setMajor(Integer user_major) {
        this.user_major = user_major;
    }

    public Integer getGrade() {
        return user_grade;
    }

    public void setGrade(Integer user_grade) {
        this.user_grade = user_grade;
    }
    public User(Integer user_id, String user_pw, Integer user_major, Integer user_grade) {
        super();

        this.user_id = user_id;
        this.user_pw = user_pw;
        this.user_major = user_major;
        this.user_grade = user_grade;
    }

    @Override
    public String toString() {
        return "User [user_no=" + user_no + ", id=" + user_id + ", password=" + user_pw + ", major=" + user_major
                + ", grade=" + user_grade + "]";
    }
}
  • 모델중 하나인 User.java입니다.
  • Spring은 @을 통해서 다른 class들과 연동이 이루어지는데 Model의 @는 @Entity입니다. 이때, 실제 DB와 연동을 해주려면 @Table(name = "DB명")또한 첨가해주어야합니다.
  • DynamicInsert나 DynamicUpdate는 실제 변경된 부분만 찾아서 입력과 수정을 해주는 JPA의 어노테이션인데 상황에 따라서 오류가 날 수 있으므로 자신의 코드를 잘 보고 사용해야합니다.
  • DB에 column에 해당하는 값은 다음과 같이 @를 붙여서 선언해주어야합니다.
@Column(name = "user_id")
private Integer user_id;
  • Primary Key는 특수하게 다음과 같이 표현해주어야 합니다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_no")
private Integer user_no;
  • 각각의 column값을 저장하는 set함수와 저장된 값을 불러오는 get함수를 생성해줍니다.
  • @을 해당하는 함수나 class 바로 위에 붙이지 않으면 인식하지 못하니 주의해야합니다.

#Repository

package com.board.back.repository;
import com.board.back.model.Class;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface ClassRepository extends JpaRepository<Class, Integer> {
    @Query(value="SELECT * from Class s where course_id in :d", nativeQuery = true)
    List<Class> printClass(@Param("d")List<Integer>d);

    @Query(value="SELECT course_id from Class s where s.course_id in :d group by s.course_id", nativeQuery = true)
    List<Integer> printClassCourseId(@Param("d")List<Integer>d);

    @Query(value="SELECT class_time from Class s where s.class_no in :d", nativeQuery = true)
    List<Integer> findCtime(@Param("d")List<Integer>d);

    @Query(value="select course_id from Class s where s.class_no in :class_no",
            nativeQuery = true)
    List<Integer> findCCID(@Param("class_no")List<Integer>class_no);
}
  • Repository는 다른 class들과 달리 interface로 구현됩니다.
  • JpaRepository를 extends하여 기능을 사용하였습니다.
  • 연동할 db와 이름과 column이 같은 모델을 import하여 인자로 받아줍니다.
  • JpaRepository에서 기본적인 save나 findAll과 같은 함수들은 제공해주지만 보다 복잡한 퀴리문을 사용하고 싶다면 @Query를 통해 직접 선언해줄 수 있습니다.
  • List<모델>의 형태로 결과값을 return해 줄 수도 있는데 이때의 select문은 select * 여야 합니다.

#Service

package com.board.back.service;

import com.board.back.exception.ResourceNotFoundException;
import com.board.back.model.*;
import com.board.back.repository.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private CourseRepository courseRepository;
    @Autowired
    private CheckFieldRepository checkFieldRepository;
    @Autowired
    private FieldRepository fieldRepository;

    @Autowired
    private UserCourseRepository UserCourseRepository;
    @Autowired
    private UserCheckFieldRepository userCheckFieldRepository;
    @Autowired
    private UserFieldRepository userFieldRepository;

    public void createUser(User user) {
        userRepository.save(user);
        //생략
}
  • @Service로 연동 시켜줍니다.
  • 호출할 Repository를 @Autowired를 통해 연결 시켜줍니다.
  • Repository의 함수를 호출하거나 새로 함수를 작성하여 기능을 수행합니다.

#Controller

package com.board.back.controller;
import com.board.back.model.User;
import com.board.back.model.User2;
import com.board.back.repository.LoginRepository;
import com.board.back.service.LoginService;
import com.board.back.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@CrossOrigin(origins = "http://localhost:3000")
@RestController
@RequestMapping("/api")
public class UserController {
    private LoginService loginService;
    private UserService userService;
    private LoginRepository loginRepository;
    @Autowired
    public UserController(UserService userService, LoginService loginService) {
        this.userService = userService;
        this.loginService = loginService;
    }

    @PostMapping("/login")
    public void loginUser(@RequestBody User user) {
        System.out.println("@PostMapping(\"/login\")");
        System.out.println(user.toString());
        loginService.postUser(user);
    }

    @GetMapping("/login2")
    public int checkUser() {
        if (loginService.getStatus() == true || loginService.getStatus() == false)
            return loginService.getNo();
        return loginService.getNo();
    }

    @PostMapping("/user")
    public void createUser(@RequestBody User user) {
        System.out.println("@PostMapping(\"/user\")");
        System.out.println(user.toString());
        userService.createUser(user);
    }
    }
  • controller에서 프론트와 데이터를 주고받아야 하기 때문에 다음과 같은 어노테이션을 달아주어야합니다.
@CrossOrigin(origins = "http://localhost:3000")
@RestController
@RequestMapping("/api")
  • 이때 3000번 포트를 사용하는 이유는 react의 default port가 3000번이기 때문이고 변경하여도 됩니다.
  • 사용할 service를 연동 시켜줍니다.
private LoginService loginService;
private UserService userService;
private LoginRepository loginRepository;
@Autowired
public UserController(UserService userService, LoginService loginService) {
	this.userService = userService;
	this.loginService = loginService;
}
  • @PostMapping("/주소")를 사용하면 프론트에서 전송한 데이터를 받아서 처리하거나 저장할 수 있습니다.
  • @GetMapping("/주소")를 사용하면 DB의 데이터를 프론트에서 전송할 수 있습니다.

#application.properties

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/DB명?serverTimezone=UTC&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=비밀번호
  • MySQL에 연동하려면 /resource/application.properties 파일에서 설정을 위와 같이 변경해주어야 합니다.
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://RDS주소:3306/graduate?serverTimezone=UTC&characterEncoding=UTF-8
spring.datasource.username=admin
spring.datasource.password=비밀번호
jpa:
database:mysql
generate-ddl:true
show-sql:true
  • local의 MySQL이 아닌 AWS의 RDS로 연동하고 싶으면 위와 같이 변경해주면 됩니다.

3. React(프론트)🪂

개발 후반부터는 React를 사용한 프론트를 주로 담당하였습니다. 툴을 Visual Studio Code를 사용하였습니다.
react에는 다양한 기술들이 들어가는데 spring과의 연동을 중점으로 설명하겠습니다.

import axios from 'axios';
class UserService {
   createUser(user) {
       return axios.post("http://localhost:8080/api/user", user);
   }
   //생략
}
  • 연동을 위해 axios를 사용하였는데 spring에서와 마찬가지로 post, get을 통하여 데이터를 주고받을 수 있습니다. 데이터는 json의 형식으로 오가게 됩니다.
  • 가끔 json형식이 아니어서 오류가 날 때가 있는데 아래와 같이 해결할 수 있습니다.
    my2(user_no){
        return axios({method :'post',
        url:'http://localhost:8080/api/my2', 
        headers :{'Content-Type': 'application/json' }, 
        data: user_no});
    }

4. AWS(호스팅)💻

마지막으로 배포에 해당하는 호스팅 작업을 AWS를 이용하여 진행하였습니다.

1. AWS 계정 생성

  • 계정을 생성하고 12개월간은 프리티어 혜택을 받을수 있습니다.
  • 서버비 지원이 안되므로 프리티어 한도 안에서 무료로 호스팅 하기 위해 프리티어 지원을 확인합니다.

2. 콘솔에 로그인

3. 지역 변경

  • 현재 거주 지역이 아니면 과금이 부과될 수 있으므로 서울로 변경시켜둡니다.

4. EC2 인스턴스 생성

  • Amazon Elastic Compute Cloud(Amazon EC2)는 Amazon Web Services(AWS) 클라우드에서 확장 가능 컴퓨팅 용량을 제공한다. Amazon EC2를 통해 원하는 만큼 가상 서버를 구축하고 보안 및 네트워크 구성과 스토리지 관리가 가능합니다. 또한 Amazon EC2는 요구 사항이나 갑작스러운 인기 증대 등 변동 사항에 따라 신속하게 규모를 확장하거나 축소할 수 있어 서버 트래픽 예측 필요성이 줄어든다.
  • 인스턴스 : 가상 컴퓨팅 환경
  • 인스턴스 생성 시작
  • ubuntu 선택
  • 프리티어 한도 내의 인스턴스 유형 선택
  • 스토리지 용량 변경
  • 8기가로 기본 설정이 되어있는데 30기가까지 가능하므로 30기가로 변경시켜준다.
  • 태그추가
  • 보안그룹 설정
  • 키 페어 다운로드
    • 검토 및 시작을 선택하면 키 페어가 생성되는데 이를 다운받은 장소를 기억해둔다.
profile
천체관측이 취미

0개의 댓글