SpringBoot 및 Vue.js 기반 졸업 작품 관리 시스템 개발

정보 기술의 급속한 발전과 함께 교육 관리의 정보화는 고등 교육 기관의 중요한 목표가 되었습니다. 기존의 수동적인 졸업 작품 관리 방식은 비효율적이고 오류 발생 가능성이 높으며, 현대 대학이 요구하는 효율적이고 정확한 관리 요구사항을 충족시키기 어렵습니다. 졸업 작품 관리는 주제 선정, 계획서 제출, 중간 점검, 최종 발표 등 여러 복잡한 단계와 방대한 데이터를 포함하므로, 프로세스를 최적화하고 효율성을 높일 체계적인 관리 도구가 절실합니다. 이러한 배경에서 SpringBoot와 Vue.js를 기반으로 하는 졸업 작품 관리 시스템을 설계하고 구현하는 것은 매우 중요합니다. 이 시스템은 졸업 작품의 전체 수명 주기를 디지털 방식으로 관리하여 수동 개입을 줄이고 관리 효율성을 향상시키는 동시에 학생과 교원에게 편리한 운영 경험을 제공합니다.

본 시스템은 프론트엔드와 백엔드가 분리된 아키텍처를 채택하고 있습니다. 백엔드는 SpringBoot 프레임워크를 기반으로 구현되었고, 프론트엔드는 Vue.js 프레임워크로 개발되었습니다. 데이터베이스는 MySQL을 사용하며, MyBatis를 통해 데이터 영속성 작업을 처리합니다. 시스템의 주요 기능 모듈은 사용자 관리, 주제 선정 관리, 과제 지시서 관리, 진행 상황 추적, 최종 발표 관리 등을 포함하여 졸업 작품의 모든 단계를 포괄합니다. 학생들은 시스템을 통해 온라인으로 주제를 선택하고 문서를 제출할 수 있으며, 교원은 과제 지시서를 검토하고 논문을 채점할 수 있습니다. 관리자는 사용자 권한을 관리하고 전체 진행 상황을 모니터링할 수 있습니다. 이 시스템은 다중 역할 협업을 지원하여 데이터 보안 및 프로세스 표준화를 보장합니다.

데이터베이스 설계

시스템의 핵심 엔티티를 나타내는 주요 데이터 테이블 구조는 다음과 같습니다.

1. 사용자 정보 테이블 (tbl_users)

사용자 등록 및 로그인 과정을 처리하며, 사용자 기본 정보 및 권한 정보를 저장합니다. 생성 시간은 시스템 함수에 의해 자동 기록됩니다.

필드명 데이터 타입 NULL 허용 설명
id BIGINT 아니오 사용자 고유 식별자 (PK)
name VARCHAR(50) 아니오 사용자 이름
login_id VARCHAR(30) 아니오 로그인 계정 ID
password_hash VARCHAR(64) 아니오 암호화된 비밀번호
role_type TINYINT 아니오 역할 유형 (1:학생, 2:교수, 3:관리자)
email VARCHAR(50) 이메일 주소
created_at DATETIME 아니오 등록 시간
updated_at DATETIME 최종 업데이트 시간

2. 졸업 작품 주제 테이블 (tbl_topics)

학생의 주제 선택 및 교수의 승인 정보를 저장합니다. 생성 시간은 시스템 함수에 의해 자동 기록됩니다.

필드명 데이터 타입 NULL 허용 설명
id BIGINT 아니오 주제 고유 식별자 (PK)
student_id BIGINT 아니오 학생 ID (FK: tbl_users.id)
advisor_id BIGINT 아니오 지도 교수 ID (FK: tbl_users.id)
title VARCHAR(100) 아니오 주제 제목
description TEXT 주제 설명
status TINYINT 아니오 상태 (0:승인 대기, 1:승인 완료, 2:반려)
created_at DATETIME 아니오 생성 시간
approved_at DATETIME 승인 시간

3. 과제 및 진행 상황 테이블 (tbl_tasks_progress)

과제 지시서 내용 및 진행 상황 정보를 저장합니다. 제출 및 업데이트 시간은 시스템 함수에 의해 자동 기록됩니다.

필드명 데이터 타입 NULL 허용 설명
id BIGINT 아니오 과제 고유 식별자 (PK)
topic_id BIGINT 아니오 관련 주제 ID (FK: tbl_topics.id)
content TEXT 아니오 과제 지시서 내용
progress_percent INT 아니오 진행률 (0-100)
submitted_at DATETIME 아니오 제출 시간
feedback_text TEXT 교수 피드백 의견
updated_at DATETIME 아니오 최종 업데이트 시간

기술 스택

백엔드 기술: Spring Boot

Spring Boot는 현대 Java 엔터프라이즈 개발의 핵심 프레임워크로, "설정보다 관례(Convention over Configuration)"라는 설계 철학을 통해 애플리케이션 개발 방식을 재정의했습니다. 주요 특징은 다음과 같습니다:

  • 간소화된 설정: 자동 설정 메커니즘을 통합하여 XML 구성 파일 작성량을 크게 줄여줍니다.
  • 내장 서버: Tomcat, Jetty, Undertow와 같은 내장 서버를 제공하여 단일 JAR 파일 배포를 지원합니다.
  • 운영 준비 기능: Actuator 모니터링 컴포넌트를 통합하여 상태 확인, 지표 수집 등 엔터프라이즈급 기능을 제공합니다.
  • 마이크로서비스 친화적: 분산 아키텍처를 자연스럽게 지원하며 Spring Cloud 생태계와 원활하게 통합됩니다.

개발자는 Starter 의존성 체계와 지능형 자동 구성을 통해 비즈니스 로직 구현에 전념할 수 있으며, 하위 인프라 구축에 대한 부담을 덜 수 있습니다. 단일 실행 가능한 JAR 배포 방식은 운영 프로세스를 크게 간소화합니다.

프론트엔드 기술: Vue.js

Vue.js는 점진적 프레임워크 설계와 뛰어난 개발 경험을 바탕으로 현대 프론트엔드 개발의 선호되는 솔루션 중 하나입니다. 주요 기술적 강점은 다음과 같습니다:

  • 반응형 데이터 흐름: 의존성 추적 기반의 반응형 시스템으로 효율적인 뷰 업데이트를 구현합니다.
  • 컴포넌트 기반 아키텍처: 단일 파일 컴포넌트(SFC) 설계를 통해 스타일, 로직, 템플릿의 완벽한 캡슐화를 가능하게 합니다.
  • 유연한 점진적 설계: 간단한 뷰 레이어 라이브러리에서 완전한 SPA(Single Page Application) 솔루션으로 확장할 수 있습니다.
  • 풍부한 생태계: Vue Router, Vuex/Pinia, Vue CLI 등 완성된 공식 도구 체인을 제공합니다.

직관적인 템플릿 구문과 강력한 지시어 시스템이 결합되어 복잡한 사용자 상호 작용을 간결하게 만들며, 우수한 TypeScript 지원과 개발자 도구는 대규모 프로젝트에 안정적인 개발 환경을 제공합니다.

핵심 코드 예시

다음은 시스템의 주요 구성 요소인 Spring Boot 애플리케이션 시작 클래스와 사용자 관리 컨트롤러의 일부 코드입니다.

1. Spring Boot 애플리케이션 시작

package com.graduation;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
@MapperScan(basePackages = {"com.graduation.mapper"})
public class GraduationProjectApplication extends SpringBootServletInitializer {

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

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(GraduationProjectApplication.class);
    }
}

2. 사용자 관리 컨트롤러

package com.graduation.controller;

import com.graduation.entity.UserEntity;
import com.graduation.service.TokenService;
import com.graduation.service.UserService;
import com.graduation.utils.PageUtils;
import com.graduation.utils.R; // Custom response wrapper
import com.graduation.annotation.IgnoreAuth; // Custom annotation for authentication bypass
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.Objects;

/**
 * 사용자 컨트롤러
 * 백엔드 API 엔드포인트
 */
@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private TokenService tokenService;

    /**
     * 사용자 로그인 처리
     */
    @IgnoreAuth
    @PostMapping("/login")
    public R login(@RequestParam String loginId, @RequestParam String password, HttpServletRequest request) {
        UserEntity user = userService.getOne(new QueryWrapper<UserEntity>().eq("login_id", loginId));
        if (user == null || !user.getPasswordHash().equals(password)) {
            return R.error("계정 또는 비밀번호가 올바르지 않습니다.");
        }
        
        // 토큰 생성 및 세션에 사용자 ID 저장
        String token = tokenService.generateToken(user.getId(), loginId, "user", user.getRoleType().toString());
        request.getSession().setAttribute("userId", user.getId());
        return R.ok().put("token", token).put("user", user);
    }

    /**
     * 새로운 사용자 회원가입 처리
     */
    @IgnoreAuth
    @PostMapping("/register")
    public R register(@RequestBody UserEntity newUser) {
        UserEntity existingUser = userService.getOne(new QueryWrapper<UserEntity>().eq("login_id", newUser.getLoginId()));
        if (existingUser != null) {
            return R.error("이미 존재하는 계정입니다.");
        }
        newUser.setId(new Date().getTime()); // 고유 ID 생성
        newUser.setCreatedAt(new Date());
        // 비밀번호 해싱 로직은 서비스 레이어 또는 엔티티에서 처리하는 것이 좋습니다.
        userService.save(newUser);
        return R.ok("회원가입이 성공적으로 완료되었습니다.");
    }

    /**
     * 사용자 로그아웃 처리
     */
    @PostMapping("/logout")
    public R logout(HttpServletRequest request) {
        request.getSession().invalidate(); // 세션 무효화
        return R.ok("로그아웃 성공");
    }

    /**
     * 현재 로그인된 사용자의 세션 정보 조회
     */
    @GetMapping("/session")
    public R getCurrentUserSession(HttpServletRequest request) {
        Long userId = (Long) request.getSession().getAttribute("userId");
        if (userId == null) {
            return R.error("세션 정보가 없습니다. 다시 로그인해주세요.");
        }
        UserEntity currentUser = userService.getById(userId);
        return R.ok().put("data", currentUser);
    }

    /**
     * 사용자 비밀번호 초기화 (기본값으로 설정)
     */
    @IgnoreAuth
    @PostMapping("/reset-password")
    public R resetPassword(@RequestParam String loginId) {
        UserEntity user = userService.getOne(new QueryWrapper<UserEntity>().eq("login_id", loginId));
        if (user == null) {
            return R.error("존재하지 않는 계정입니다.");
        }
        user.setPasswordHash("123456"); // 임시 비밀번호 설정
        user.setUpdatedAt(new Date());
        userService.updateById(user);
        return R.ok("비밀번호가 '123456'으로 초기화되었습니다.");
    }

    /**
     * 관리자 페이지용 사용자 목록 조회 (필터링 및 페이지네이션 지원)
     */
    @GetMapping("/admin/list")
    public R getAdminUsers(@RequestParam Map<String, Object> params) {
        QueryWrapper<UserEntity> queryWrapper = new QueryWrapper<>();
        
        // 검색 조건 적용
        if (params.containsKey("loginId") && !Objects.requireNonNullElse(params.get("loginId"), "").toString().isEmpty()) {
            queryWrapper.like("login_id", params.get("loginId"));
        }
        if (params.containsKey("name") && !Objects.requireNonNullElse(params.get("name"), "").toString().isEmpty()) {
            queryWrapper.like("name", params.get("name"));
        }
        if (params.containsKey("roleType") && !Objects.requireNonNullElse(params.get("roleType"), "").toString().isEmpty()) {
            queryWrapper.eq("role_type", params.get("roleType"));
        }

        // 페이지네이션 설정
        int pageNum = Integer.parseInt(Objects.requireNonNullElse(params.get("page"), "1").toString());
        int pageSize = Integer.parseInt(Objects.requireNonNullElse(params.get("limit"), "10").toString());

        Page<UserEntity> userPage = new Page<>(pageNum, pageSize);
        userService.page(userPage, queryWrapper); // MyBatis Plus의 페이지네이션 메소드
        
        // 커스텀 PageUtils 객체로 변환하여 반환 (원래 시스템의 R.ok() 형식 유지)
        return R.ok().put("data", new PageUtils(userPage.getRecords(), userPage.getTotal(), userPage.getSize(), userPage.getCurrent()));
    }

    /**
     * 프론트엔드용 공개 사용자 목록 조회 (인증 불필요, 제한된 필터링)
     */
    @IgnoreAuth
    @GetMapping("/public/list")
    public R getPublicUsers(@RequestParam Map<String, Object> params) {
        QueryWrapper<UserEntity> queryWrapper = new QueryWrapper<>();
        if (params.containsKey("name") && !Objects.requireNonNullElse(params.get("name"), "").toString().isEmpty()) {
            queryWrapper.like("name", params.get("name"));
        }
        
        int pageNum = Integer.parseInt(Objects.requireNonNullElse(params.get("page"), "1").toString());
        int pageSize = Integer.parseInt(Objects.requireNonNullElse(params.get("limit"), "10").toString());

        Page<UserEntity> userPage = new Page<>(pageNum, pageSize);
        userService.page(userPage, queryWrapper);
        return R.ok().put("data", new PageUtils(userPage.getRecords(), userPage.getTotal(), userPage.getSize(), userPage.getCurrent()));
    }

    /**
     * 특정 사용자 상세 정보 조회 (관리자용)
     */
    @GetMapping("/admin/detail/{id}")
    public R getAdminUserDetail(@PathVariable("id") Long id) {
        UserEntity user = userService.getById(id);
        return R.ok().put("data", user);
    }

    /**
     * 특정 사용자 상세 정보 조회 (공개, 인증 불필요)
     */
    @IgnoreAuth
    @GetMapping("/public/detail/{id}")
    public R getPublicUserDetail(@PathVariable("id") Long id) {
        UserEntity user = userService.getById(id);
        return R.ok().put("data", user);
    }
    
    /**
     * 새로운 사용자 정보 추가 (관리자 권한 필요)
     */
    @PostMapping("/admin/add")
    public R addUser(@RequestBody UserEntity newUser) {
        if (userService.count(new QueryWrapper<UserEntity>().eq("login_id", newUser.getLoginId())) > 0) {
            return R.error("이미 존재하는 로그인 ID입니다.");
        }
        newUser.setId(new Date().getTime() + (long) (Math.random() * 1000)); // 고유 ID 생성
        newUser.setCreatedAt(new Date());
        userService.save(newUser);
        return R.ok("사용자 정보가 성공적으로 추가되었습니다.");
    }

    /**
     * 기존 사용자 정보 수정 (관리자 권한 필요)
     */
    @PutMapping("/admin/update")
    @Transactional
    public R updateUser(@RequestBody UserEntity userToUpdate) {
        if (userToUpdate.getId() == null) {
            return R.error("업데이트할 사용자 ID가 필요합니다.");
        }
        // 다른 사용자가 동일한 로그인 ID를 사용하는지 확인
        if (userService.count(new QueryWrapper<UserEntity>()
                .ne("id", userToUpdate.getId())
                .eq("login_id", userToUpdate.getLoginId())) > 0) {
            return R.error("다른 사용자가 이미 해당 로그인 ID를 사용 중입니다.");
        }
        userToUpdate.setUpdatedAt(new Date());
        userService.updateById(userToUpdate);
        return R.ok("사용자 정보가 성공적으로 업데이트되었습니다.");
    }

    /**
     * 선택된 사용자 정보 삭제 (관리자 권한 필요)
     */
    @DeleteMapping("/admin/delete")
    public R deleteUsers(@RequestBody Long[] ids) {
        userService.removeByIds(Arrays.asList(ids));
        return R.ok("선택된 사용자(들)이 성공적으로 삭제되었습니다.");
    }
}

태그: SpringBoot Vue.js MySQL MyBatis java

7월 20일 05:07에 게시됨