SpringBoot, Vue.js, Element-UI 기반 온라인 블로그 시스템 개발

1. 시스템 개요

이 온라인 블로그 시스템은 현대 웹 개발 기술 스택을 활용하여 설계 및 구현되었습니다. 백엔드는 Spring Boot 프레임워크를 기반으로 하며, 데이터 지속성을 위해 MyBatis-Plus를 사용하고, 사용자 인증 및 권한 관리는 Spring Security 기술로 제어합니다. 데이터베이스는 MySQL을 채택했습니다. 프론트엔드는 Vue 2.x, Element-UI, Axios, ECharts 등의 기술로 구축되었으며, 관리 시스템은 Vue-Element-Admin을 활용합니다.

프론트엔드 개발 환경으로는 Node.js 14.21.3 버전을 권장합니다.

2. 핵심 기술 소개

2.1 Java 언어

Java는 Oracle 사가 개발한 객체 지향 프로그래밍 언어로, 1995년 출시 이후 웹 애플리케이션, 데스크톱 애플리케이션 등 다양한 분야에서 널리 사용되고 있습니다. Java의 주요 특징은 다음과 같습니다:

  • 객체 지향: 클래스, 인터페이스, 상속 등 객체 지향 패러다임을 지원하며, 단일 상속 및 다중 인터페이스 상속, 동적 바인딩을 제공합니다.
  • 분산 환경 지원: 인터넷 애플리케이션 개발에 적합하며, 네트워크 프로그래밍 인터페이스(java.net) 및 RMI(원격 메서드 호출) 메커니즘을 지원하여 분산 애플리케이션 개발에 용이합니다.
  • 견고성: 강력한 타입 검사, 예외 처리 메커니즘, 자동 가비지 컬렉션 등은 프로그램의 안정성을 보장합니다. 또한, 보안 검사 메커니즘은 견고성을 강화합니다.
  • 보안성: 악성 코드 공격을 방지하기 위한 클래스 로더의 보안 방어 메커니즘 및 보안 관리 메커니즘을 제공합니다.
  • 아키텍처 중립성: Java 프로그램은 아키텍처 중립적인 바이트코드 형식으로 컴파일되어, Java 플랫폼을 구현하는 모든 시스템에서 실행 가능하여 이기종 네트워크 환경 및 소프트웨어 배포에 적합합니다.
  • 이식성: 아키텍처 중립성과 엄격하게 정의된 기본 데이터 타입의 길이로 이식성을 보장합니다. Java 컴파일러 및 실행 환경 또한 Java로 구현되어 이식성을 더욱 향상시킵니다.
  • 인터프리터 방식: Java 코드는 먼저 바이트코드로 컴파일된 후 Java 가상 머신(JVM)에 의해 해석 및 실행됩니다. JVM은 대부분의 소프트웨어 및 하드웨어 플랫폼에서 사용 가능하여 Java 코드의 높은 이식성을 실현합니다.

2.2 HTML 웹 기술

HTML(HyperText Markup Language)은 1990년에 개발된 마크업 언어입니다. 일련의 태그를 통해 인터넷상의 문서 형식을 통일하고, 분산된 인터넷 자원을 논리적인 전체로 연결합니다. HTML 텍스트는 HTML 명령으로 구성된 설명 텍스트이며, 웹 브라우저를 통해 표시되어야 합니다. HTML은 웹 페이지 파일을 생성하는 언어로, 태그 기반 지시어(Tag)를 통해 텍스트, 그래픽, 애니메이션, 사운드, 테이블, 링크, 이미지 등을 표시합니다.

2.3 MySQL 데이터베이스

MySQL은 여러 차례의 업데이트를 거쳐 기능이 풍부하고 완벽해졌습니다. 특히 MySQL 4.x에서 5.x 버전으로의 주요 업데이트는 상업적 활용에서 큰 성공을 거두었습니다. 최신 MySQL 버전은 정보 압축 및 암호화를 지원하여 정보 보안 요구사항을 더욱 효과적으로 충족합니다. 또한, 시스템 업데이트를 통해 데이터베이스 자체의 미러링 기능이 크게 향상되었고, 실행 유연성 및 사용 편의성도 크게 개선되었습니다. 드라이버 사용 및 생성도 더욱 효율적이고 빨라졌습니다. 가장 큰 변화는 공간 정보 표시 최적화로, 지도 애플리케이션에서 좌표 표기 및 계산을 더욱 편리하게 수행할 수 있게 되었습니다. 강력한 백업 기능은 사용자에게 안심하고 사용할 수 있는 환경을 제공하며, Office 특성을 지원하여 사용자가 직접 설치하고 사용할 수도 있습니다. 정보 표시 형식 또한 크게 개선되어, 정보 영역과 계기판 정보 컨트롤이라는 두 가지 매우 유용한 표시 영역이 추가되었습니다. 정보 영역은 테이블과 텍스트를 분류 처리하여 인터페이스를 더욱 깔끔하고 구체적으로 표시하며, 계기판 정보 컨트롤은 여러 정보를 비교하여 사용자에게 큰 편의를 제공합니다.

2.4 Spring Boot 프레임워크

Spring Boot는 Pivotal 팀이 2013년에 개발한 새로운 프레임워크로, 새로운 Spring 애플리케이션의 초기 설정 및 개발 프로세스를 단순화하는 것을 목표로 합니다. Spring 프레임워크는 Java 플랫폼용 오픈소스 애플리케이션 프레임워크로, 제어 역전(IoC) 특성을 가진 컨테이너를 제공합니다. Spring Boot는 Spring 4.0을 기반으로 설계되었으며, 기존 Spring 프레임워크의 뛰어난 특성을 계승할 뿐만 아니라, 설정을 간소화하여 Spring 애플리케이션의 전체 설정 및 개발 과정을 더욱 단순화합니다. 또한, Spring Boot는 수많은 프레임워크를 통합하여 의존성 패키지 버전 충돌 및 참조 불안정성 등의 문제를 효과적으로 해결합니다.

Spring Boot의 특징은 다음과 같습니다:

  • 독립적인 Spring 애플리케이션을 생성할 수 있으며, Maven 또는 Gradle 플러그인을 기반으로 실행 가능한 JAR 및 WAR 파일을 생성합니다.
  • Tomcat 또는 Jetty와 같은 서블릿 컨테이너를 내장합니다.
  • Maven 설정을 단순화하기 위해 자동 구성 "스타터" 프로젝트 객체 모델(POM)을 제공합니다.
  • 가능한 한 Spring 컨테이너를 자동 구성합니다.
  • 메트릭, 상태 확인, 외부화된 구성과 같은 준비된 기능을 제공합니다.
  • 코드 생성이나 XML 구성이 전혀 필요 없습니다.

2.5 Vue.js 프레임워크

Vue.js는 사용자 인터페이스 구축을 위한 JavaScript 프레임워크입니다. 표준 HTML, CSS, JavaScript를 기반으로 구축되었으며, 선언적이고 컴포넌트화된 프로그래밍 모델을 제공하여 효율적인 사용자 인터페이스 개발을 가능하게 합니다. Vue.js는 사용자 인터페이스를 구축하기 위한 점진적 프레임워크로, 상향식 증분 개발 설계를 채택하여 핵심 라이브러리는 뷰 레이어에만 집중합니다. 또한, Vue는 단일 파일 컴포넌트와 Vue 생태계가 지원하는 라이브러리를 사용하여 개발된 복잡한 단일 페이지 애플리케이션을 구동할 수 있는 완전한 능력을 가지고 있습니다.

Vue.js의 주요 특징은 다음과 같습니다:

  • 반응형 데이터 바인딩 시스템: 애플리케이션의 데이터가 변경될 때 페이지의 관련 부분이 자동으로 업데이트됩니다. 이 메커니즘은 개발자가 복잡한 애플리케이션 상태를 쉽게 관리하고 유지 보수할 수 있도록 합니다.
  • 컴포넌트 기반 개발: 개발자는 페이지를 독립적인 컴포넌트로 분할할 수 있으며, 각 컴포넌트는 자체 데이터와 로직을 가집니다. 이러한 모듈식 개발 방식은 코드 재사용 및 유지 보수를 용이하게 하고 개발 효율성을 향상시킵니다.
  • 강력한 디렉티브 시스템: DOM 조작 및 페이지 상호 작용을 처리하기 위한 강력한 디렉티브 시스템을 제공합니다. 이러한 디렉티브를 사용하여 개발자는 조건부 렌더링, 반복 렌더링, 이벤트 처리 등과 같은 기능을 쉽게 구현할 수 있습니다.
  • 풍부한 플러그인 생태계: 라우팅, 상태 관리, 폼 유효성 검사 등 다양한 플러그인을 사용하여 Vue.js의 기능을 확장할 수 있습니다. 이를 통해 개발자는 프로젝트 요구 사항에 맞는 플러그인을 선택하여 요구 사항을 더 잘 충족시킬 수 있습니다.

2.6 Element-UI 프레임워크

Element-UI는 Ele.me 프론트엔드 팀이 Vue.js 2.0을 기반으로 출시한 데스크톱 UI 컴포넌트 라이브러리입니다. 개발자에게 완전하고 사용하기 쉽고 아름다운 컴포넌트 솔루션을 제공하여 프론트엔드 개발의 효율성과 품질을 크게 향상시킵니다.

Element-UI의 특징은 다음과 같습니다:

  • 완전성: 기본 컨트롤부터 복잡한 컴포넌트까지 포괄적인 UI 솔루션을 제공하며, 폼, 테이블, 대화 상자, 메시지 프롬프트 등을 포함합니다. 이 컴포넌트들은 일반적인 UI 시나리오를 커버하므로 개발자는 반복적인 작업을 줄이고 직접 사용할 수 있습니다.
  • 사용 편의성: Element-UI의 API 설계는 간단하고 직관적이며, 문서가 상세하고 풍부하여 개발자가 빠르게 익숙해질 수 있습니다. Vue.js와 원활하게 통합되어 Vue의 기능(예: 데이터 바인딩, 컴포넌트화)을 활용하여 개발을 더욱 편리하게 합니다.
  • 아름다운 디자인: Element-UI의 디자인 스타일은 간결하고 우아하며, 현대 UI 디자인 트렌드에 부합합니다. 다양한 테마 구성을 제공하여 개발자는 프로젝트 요구 사항에 따라 사용자 정의할 수 있습니다.
  • 확장성: Element-UI는 풍부한 훅 함수와 이벤트를 제공하여 2차 개발 및 확장을 용이하게 합니다. 개발자는 Element-UI 컴포넌트를 기반으로 특정 비즈니스 요구 사항을 충족하는 사용자 정의 개발을 수행할 수 있습니다.

3. 시스템 분석

3.1 기술적 실현 가능성

본 시스템은 Java 프로그래밍 언어, Spring Boot 백엔드 프레임워크, Vue.js 프론트엔드 프레임워크 기술 및 MySQL 데이터베이스를 기반으로 개발 및 설계됩니다. 이러한 기술 스택은 현재 웹 개발 분야에서 널리 사용되고 안정성이 검증된 기술들이므로, 시스템 개발에 대한 기술적 익숙도와 이해도가 높다고 판단하여 기술적 실현 가능성이 충분합니다.

3.2 시스템 성능 요구사항

  • 시스템 응답 효율성: 페이지 응답 시간은 3초 이내여야 하며, 최대 4초를 넘지 않아야 합니다. 최소 10,000명의 동시 접속자를 지원해야 합니다.
  • 간결하고 명확한 인터페이스: 시스템 인터페이스는 간단하고 명료하며, 조작이 용이하고 사용자 조작 습관에 부합해야 합니다.
  • 높은 저장 용량: 본 시스템에는 많은 정보가 저장되어야 하므로, 강력한 데이터베이스 지원을 통해 모든 정보가 안전하고 안정적으로 저장될 수 있도록 충분한 저장 용량이 요구됩니다.
  • 쉬운 학습성: 시스템 조작은 간단하고 쉽게 익힐 수 있어야 하며, 복잡한 조작 없이 간단한 학습만으로 시스템을 사용할 수 있어야 합니다.
  • 안정성: 개발된 시스템은 안정적으로 실행되어야 하며, 실행 과정에서 인터페이스가 불분명하거나 글꼴이 흐릿하거나 정상적으로 조작할 수 없는 현상이 없어야 합니다.

3.3 시스템 기능 모듈

온라인 블로그 시스템은 주로 다음과 같은 주요 기능 모듈로 구성됩니다:

  • 사용자 관리: 시스템 관리자와 일반 사용자를 포함합니다. 관리자는 시스템의 모든 기능을 동적으로 관리할 수 있습니다. 일반 사용자는 개인 정보 수정, 게시글 관리, 댓글 관리 등이 가능합니다.
  • 메뉴 관리: 시스템의 모든 메뉴를 동적으로 관리하여 시스템의 낮은 결합도 설계를 구현합니다.
  • 권한 관리: 역할 관리, 역할 권한 동적 할당 등을 통해 시스템 사용자 권한을 동적으로 제어하여, 사용자 및 역할에 따른 권한을 차등 부여합니다. 각 사용자는 자신의 정보만 관리할 수 있습니다. 이를 통해 시스템 보안 및 사용자 데이터 보안을 보장하고 시스템의 견고성을 제공합니다.
  • 게시글 관리: 게시글을 관리하며, 각 사용자는 자신의 게시글만 관리할 수 있습니다. 관리자는 모든 게시글을 관리할 수 있습니다.
  • 게시글 분류 관리: 관리자가 게시글 분류를 통일적으로 관리합니다.
  • 태그 관리: 게시글에 태그를 추가할 수 있으며, 관리자가 게시글 태그를 통일적으로 관리합니다.
  • 댓글 관리: 사용자가 게시글에 댓글을 달고, 좋아요를 누르며, 후원하는 등의 기능을 추가합니다.
  • 댓글 심사 관리: 댓글을 수동 또는 자동으로 필터링하여 심사하는 기능을 제공합니다. 자동 심사 시 플랫폼은 민감한 단어를 *로 처리합니다.
  • 개인 센터: 사용자의 개인 정보 및 활동을 관리합니다.
  • 백엔드 관리: 관리자가 시스템 전반을 관리하는 기능입니다.

3.4 시스템 핵심 프로세스

시스템의 주요 운영 흐름은 다음과 같습니다:

  • 로그인 프로세스: 사용자가 로그인 정보를 입력하면 시스템은 사용자명과 비밀번호를 검증합니다. 유효한 경우 사용자 유형(관리자/일반 사용자)에 따라 적절한 대시보드로 이동시키고, 유효하지 않은 경우 오류 메시지를 표시합니다.
  • 회원 가입 프로세스: 신규 사용자가 등록 페이지에서 필요한 정보를 입력하고 제출하면, 시스템은 입력된 데이터를 검증하고 중복 사용자 여부를 확인합니다. 유효하고 중복이 없는 경우 사용자 계정을 생성하고 데이터베이스에 저장합니다.
  • 정보 추가 프로세스: 사용자가 새로운 정보를 추가할 때, 시스템은 정보에 대한 유효성 검사를 수행합니다. 검증이 통과되면 해당 정보는 데이터베이스에 저장되고 성공 메시지가 표시됩니다. 실패 시 오류 메시지가 반환됩니다.
  • 정보 삭제 프로세스: 사용자가 삭제할 정보를 선택하면, 시스템은 삭제 확인 프롬프트를 표시합니다. 사용자가 삭제를 확정하면 해당 정보는 데이터베이스에서 제거되고 성공 메시지가 표시됩니다.

4. 시스템 설계

4.1 시스템 개요 설계

시스템은 B/S(브라우저/서버) 아키텍처를 채택하여 웹 브라우저를 통해 접근 및 조작이 가능합니다. 시스템은 크게 두 부분으로 구성됩니다:

  • 온라인 블로그 웹사이트: 사용자가 게시글을 검색, 조회, 댓글 작성, 좋아요, 후원하는 등 학습 및 교류를 위한 인터페이스를 제공합니다.
  • 백엔드 관리 시스템: 관리자에게 사용자 관리, 게시글 관리, 댓글 관리, 데이터 통계 등 시스템 관리 인터페이스를 제공합니다.

4.2 시스템 구조 설계

전체 시스템은 여러 기능 모듈의 조합으로 구성되며, 각 모듈은 고유한 기능 설계에 따라 구현되고 전체 시스템의 설계에 통합됩니다.

4.3 데이터베이스 설계

효율적인 데이터베이스는 프로그램 개발의 품질에 직접적인 영향을 미칩니다. 데이터베이스 설계는 테이블 구조, 테이블 간 관계, 시스템 개발에 필요한 데이터 테이블 내용 등을 포함합니다. MySQL 데이터베이스를 채택하여 데이터 저장 속도를 확보하고, 정보 내용이 많은 블로그 시스템의 특성을 고려하여 정보 분류가 명확하고 혼란스럽지 않도록 데이터베이스를 설계했습니다.

주요 관계 모델의 논리적 구조는 다음과 같습니다:

  • 사용자 정보: (기본키 id, 사용자 id, 사용자 이름, 테이블 이름, 역할 id, 역할 이름, 비밀번호, 생성 시간, 만료 시간)
  • 역할 정보: (기본키 id, 역할 이름, 역할 설명, 생성 시간, 만료 시간)
  • 역할-권한 정보: (기본키 id, 역할 id, 역할 이름, 연결된 메뉴 id, 연결된 메뉴 이름, 생성 시간, 만료 시간)
  • 메뉴 정보: (기본키 id, 메뉴 이름, 메뉴 접근 경로, 메뉴 설명, 상위 메뉴 id, 생성 시간, 만료 시간)
  • 게시글 정보: (기본키 id, 생성일, 게시글 제목, 게시글 내용, 게시글 유형, 게시글 태그, 좋아요 수, 조회수, 부모 노드 id, 업로드 사용자 id, 업로드 사용자 이름, 상태, 게시글 심사 상태, 심사자 id, 심사자)
  • 댓글 정보: (기본키 id, 생성일, 게시글 id, 게시글 제목, 댓글 작성자 id, 댓글 작성자 이름, 댓글 내용, 심사 여부, 댓글 시간 등)

5. 시스템 상세 구현

5.1 사용자 웹사이트

  • 사용자 로그인 및 등록 모듈: 사용자는 로그인 정보를 입력하여 시스템에 접속합니다.
  • 사용자 센터: 로그인한 사용자가 자신의 정보, 게시글, 댓글 등을 관리합니다.

5.2 관리자 백엔드

관리자는 시스템의 모든 기능에 접근하여 사용자, 게시글, 분류, 태그, 댓글 등을 관리할 수 있습니다.

6. 주요 코드 구현 예시

6.1 Spring Boot 구성 파일 (application.yml)


# Tomcat 서버 설정
server:
    tomcat:
        uri-encoding: UTF-8 # URI 인코딩 설정
    port: 8080 # 서버 포트
    servlet:
        context-path: /blog_platform_api # 애플리케이션 컨텍스트 경로

# Spring 데이터소스 설정 (MySQL)
spring:
    datasource:
        driverClassName: com.mysql.cj.jdbc.Driver # MySQL JDBC 드라이버
        url: jdbc:mysql://127.0.0.1:3306/online_blog_db?useUnicode=true&characterEncoding=utf-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Seoul # 데이터베이스 URL
        username: root # 데이터베이스 사용자 이름
        password: secure_password # 데이터베이스 비밀번호
    
    # 멀티파트 파일 업로드 설정
    servlet:
      multipart:
        max-file-size: 15MB # 최대 파일 크기
        max-request-size: 15MB # 최대 요청 크기
    # 정적 리소스 위치 설정
    resources:
      static-locations: classpath:/static/,classpath:/public/,classpath:/resources/,classpath:/META-INF/resources/

# MyBatis-Plus 설정
mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml # 매퍼 XML 파일 위치
  typeAliasesPackage: com.app.entity # 엔티티 스캔 패키지
  global-config:
    id-type: 1 # 주키 타입 (0: DB 자동 증가, 1: 사용자 입력, 2: 전역 고유 ID, 3: UUID)
    field-strategy: 2 # 필드 전략 (0: 무시, 1: NULL 아님, 2: 비어있지 않음)
    db-column-underline: true # 카멜 케이스 -> 스네이크 케이스 자동 변환
    refresh-mapper: true # 매퍼 새로 고침 (디버깅용)
    logic-delete-value: -1 # 논리적 삭제 값
    logic-not-delete-value: 0 # 논리적 비삭제 값
    sql-injector: com.baomidou.mybatisplus.mapper.LogicSqlInjector # 사용자 정의 SQL 인젝터
  configuration:
    map-underscore-to-camel-case: true # 언더스코어 -> 카멜 케이스 매핑
    cache-enabled: false # 캐시 비활성화
    call-setters-on-nulls: true # NULL 값에 대해서도 setter 호출
    jdbc-type-for-null: 'null' # NULL 값에 대한 JDBC 타입 설정 (Oracle 등 특정 DB용)

6.2 사용자 인증 및 관리 컨트롤러 (Java)


import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import com.alibaba.fastjson.JSONObject; // 빠른 JSON 처리
import com.app.entity.SystemUser; // 사용자 엔티티
import com.app.entity.UserGroup; // 사용자 그룹 엔티티
import com.app.service.AuthTokenService; // 토큰 서비스
import com.app.service.SystemUserService; // 사용자 서비스
import com.app.service.UserGroupService; // 사용자 그룹 서비스
import com.app.util.BaseController; // 공통 컨트롤러 기능
import com.app.util.QueryParam; // 쿼리 파라미터 유틸
import com.app.util.ResultResponse; // 응답 결과 유틸

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * 시스템 사용자 계정 관리 컨트롤러 (SystemUser)
 */
@Slf4j
@RestController
@RequestMapping("/user_account")
public class UserAuthController extends BaseController<SystemUser, SystemUserService> {

    private final SystemUserService systemUserService;
    private final AuthTokenService authTokenService;
    private final UserGroupService userGroupService;

    @Autowired
    public UserAuthController(SystemUserService service, AuthTokenService tokenService, UserGroupService groupService) {
        super.setService(service);
        this.systemUserService = service;
        this.authTokenService = tokenService;
        this.userGroupService = groupService;
    }

    /**
     * 사용자 등록
     * @param newUser 등록할 사용자 정보
     * @return 등록 결과
     */
    @PostMapping("/register_new")
    public Map<String, Object> registerUser(@RequestBody SystemUser newUser) {
        if (newUser.getUsername() == null || newUser.getUsername().isEmpty()) {
            return ResultResponse.error(40001, "사용자 이름은 필수입니다.");
        }

        Map<String, String> query = new HashMap<>();
        query.put("username", newUser.getUsername());
        List<SystemUser> existingUsers = systemUserService.fetchRecords(query, new HashMap<>()).getResults();
        
        if (!existingUsers.isEmpty()) {
            return ResultResponse.error(40002, "이미 존재하는 사용자 이름입니다.");
        }

        newUser.setUserId(null); // DB에서 자동 생성
        newUser.setPasswordHash(systemUserService.hashPassword(newUser.getPasswordHash())); // 비밀번호 해싱
        systemUserService.saveRecord(newUser);
        return ResultResponse.success("회원 가입 성공", 1);
    }

    /**
     * 비밀번호 재설정 요청
     * @param resetForm 비밀번호 재설정 정보 (사용자명, 코드, 새 비밀번호)
     * @return 재설정 결과
     */
    @PostMapping("/password_reset")
    public Map<String, Object> resetPassword(@RequestBody SystemUser resetForm, HttpServletRequest request) {
        String username = resetForm.getUsername();
        String verificationCode = resetForm.getVerificationCode(); // 가상의 검증 코드 필드
        String newPassword = resetForm.getPasswordHash();

        if (StringUtils.isEmpty(verificationCode)) {
            return ResultResponse.error(40003, "인증 코드가 필요합니다.");
        }
        if (StringUtils.isEmpty(username)) {
            return ResultResponse.error(40004, "사용자 이름은 필수입니다.");
        }
        if (StringUtils.isEmpty(newPassword)) {
            return ResultResponse.error(40005, "새 비밀번호는 필수입니다.");
        }

        Map<String, String> userQuery = new HashMap<>();
        userQuery.put("username", username);
        QueryParam userSearch = systemUserService.fetchRecords(userQuery, systemUserService.getConfigParams(request));
        List<SystemUser> foundUsers = userSearch.getResults();

        if (foundUsers.isEmpty()) {
            return ResultResponse.error(40006, "해당 사용자가 존재하지 않습니다.");
        }

        // 실제 환경에서는 verificationCode 검증 로직이 추가되어야 함
        
        SystemUser userToUpdate = foundUsers.get(0);
        Map<String, Object> updateFields = new HashMap<>();
        updateFields.put("password_hash", systemUserService.hashPassword(newPassword));
        
        systemUserService.updateRecord(userQuery, systemUserService.getConfigParams(request), updateFields);
        return ResultResponse.success("비밀번호 재설정 완료", 1);
    }

    /**
     * 사용자 로그인
     * @param credentials 로그인 자격 증명 (사용자명/이메일/전화번호, 비밀번호)
     * @param request HTTP 요청 객체
     * @return 로그인 결과 (토큰 및 사용자 정보)
     */
    @PostMapping("/authenticate")
    public Map<String, Object> authenticateUser(@RequestBody Map<String, String> credentials, HttpServletRequest request) {
        log.info("사용자 로그인 시도: {}", credentials.get("username"));

        String username = credentials.get("username");
        String email = credentials.get("email");
        String phone = credentials.get("phone");
        String password = credentials.get("password");

        if (StringUtils.isEmpty(password) || (StringUtils.isEmpty(username) && StringUtils.isEmpty(email) && StringUtils.isEmpty(phone))) {
            return ResultResponse.error(40007, "계정 정보와 비밀번호를 모두 입력해주세요.");
        }

        Map<String, String> userLookup = new HashMap<>();
        if (!StringUtils.isEmpty(username)) userLookup.put("username", username);
        else if (!StringUtils.isEmpty(email)) userLookup.put("email", email);
        else if (!StringUtils.isEmpty(phone)) userLookup.put("phone", phone);

        List<SystemUser> matchedUsers = systemUserService.fetchRecords(userLookup, new HashMap<>()).getResults();

        if (matchedUsers.isEmpty()) {
            return ResultResponse.error(40008, "사용자가 존재하지 않습니다.");
        }

        SystemUser foundUser = matchedUsers.get(0);
        
        Map<String, String> groupLookup = new HashMap<>();
        groupLookup.put("name", foundUser.getUserGroup());
        List<UserGroup> userGroups = userGroupService.fetchRecords(groupLookup, new HashMap<>()).getResults();

        if (userGroups.isEmpty()) {
            return ResultResponse.error(40009, "유효하지 않은 사용자 그룹입니다.");
        }

        UserGroup assignedGroup = userGroups.get(0);

        // 사용자 심사 상태 확인
        if (!StringUtils.isEmpty(assignedGroup.getSourceTable())) {
            String checkSql = "SELECT approval_status FROM " + assignedGroup.getSourceTable() + " WHERE user_id = " + foundUser.getUserId();
            Object statusResult = systemUserService.executeSingleSql(checkSql).getSingleResult();
            if (statusResult == null || !"승인됨".equals(String.valueOf(statusResult))) {
                return ResultResponse.error(40010, "사용자 계정이 승인 대기 중이거나 거부되었습니다.");
            }
        }

        // 사용자 활성 상태 확인
        if (foundUser.getAccountStatus() != 1) { // 1은 활성 상태를 의미한다고 가정
            return ResultResponse.error(40011, "비활성화된 계정은 로그인할 수 없습니다.");
        }

        String hashedPasswordInput = systemUserService.hashPassword(password);
        if (foundUser.getPasswordHash().equals(hashedPasswordInput)) {
            // 새 인증 토큰 생성 및 저장
            AuthToken newAuthToken = new AuthToken();
            newAuthToken.setTokenValue(UUID.randomUUID().toString().replaceAll("-", ""));
            newAuthToken.setUserId(foundUser.getUserId());
            authTokenService.saveRecord(newAuthToken);

            JSONObject userDetails = JSONObject.parseObject(JSONObject.toJSONString(foundUser));
            userDetails.put("authToken", newAuthToken.getTokenValue());
            JSONObject responsePayload = new JSONObject();
            responsePayload.put("user_info", userDetails);
            return ResultResponse.success("로그인 성공", responsePayload);
        } else {
            return ResultResponse.error(40012, "계정 정보 또는 비밀번호가 올바르지 않습니다.");
        }
    }

    /**
     * 사용자 비밀번호 변경
     * @param passData 비밀번호 변경 정보 (이전 비밀번호, 새 비밀번호)
     * @param request HTTP 요청 객체
     * @return 변경 결과
     */
    @PostMapping("/update_password")
    public Map<String, Object> updatePassword(@RequestBody Map<String, String> passData, HttpServletRequest request) {
        String currentToken = request.getHeader("x-auth-token");
        Integer userId = getUserIdFromToken(currentToken);

        if (userId == null || userId == 0) {
            return ResultResponse.error(40013, "로그인 정보가 유효하지 않습니다.");
        }

        String oldPassword = passData.get("old_password");
        String newPassword = passData.get("new_password");

        if (StringUtils.isEmpty(oldPassword) || StringUtils.isEmpty(newPassword)) {
            return ResultResponse.error(40014, "현재 비밀번호와 새 비밀번호를 모두 입력해주세요.");
        }

        Map<String, String> userMatchQuery = new HashMap<>();
        userMatchQuery.put("user_id", String.valueOf(userId));
        userMatchQuery.put("password_hash", systemUserService.hashPassword(oldPassword));
        
        int matchingUsers = systemUserService.countRecords(userMatchQuery, systemUserService.getConfigParams(request)).getTotalCount();
        
        if (matchingUsers > 0) {
            Map<String, Object> updateMap = new HashMap<>();
            updateMap.put("password_hash", systemUserService.hashPassword(newPassword));
            systemUserService.updateRecord(userMatchQuery, systemUserService.getConfigParams(request), updateMap);
            return ResultResponse.success("비밀번호가 성공적으로 변경되었습니다.", 1);
        }
        return ResultResponse.error(40015, "현재 비밀번호가 올바르지 않습니다.");
    }

    /**
     * 로그인 상태 확인
     * @param request HTTP 요청 객체
     * @return 로그인 상태 및 사용자 정보
     */
    @GetMapping("/check_auth_state")
    public Map<String, Object> checkAuthState(HttpServletRequest request) {
        String authToken = request.getHeader("x-auth-token");
        Integer userId = getUserIdFromToken(authToken);

        if (userId == null || userId == 0) {
            return ResultResponse.error(40016, "사용자가 로그인되어 있지 않습니다.");
        }

        Map<String, String> userLookup = new HashMap<>();
        userLookup.put("user_id", String.valueOf(userId));
        List<SystemUser> activeUsers = systemUserService.fetchRecords(userLookup, systemUserService.getConfigParams(request)).getResults();
        
        if (!activeUsers.isEmpty()) {
            JSONObject userProfile = JSONObject.parseObject(JSONObject.toJSONString(activeUsers.get(0)));
            userProfile.put("authToken", authToken);
            JSONObject response = new JSONObject();
            response.put("user_details", userProfile);
            return ResultResponse.success("로그인 상태 유지", response);
        } else {
            return ResultResponse.error(40017, "유효한 로그인 세션을 찾을 수 없습니다.");
        }
    }

    /**
     * 사용자 로그아웃
     * @param request HTTP 요청 객체
     * @return 로그아웃 결과
     */
    @GetMapping("/logout")
    public Map<String, Object> logoutUser(HttpServletRequest request) {
        String tokenToInvalidate = request.getHeader("x-auth-token");
        Map<String, String> tokenQuery = new HashMap<>(1);
        tokenQuery.put("token_value", tokenToInvalidate);
        try {
            authTokenService.deleteRecords(tokenQuery, systemUserService.getConfigParams(request));
        } catch (Exception e) {
            log.error("로그아웃 중 토큰 삭제 오류 발생: {}", e.getMessage());
            return ResultResponse.error(50000, "로그아웃 처리 중 오류가 발생했습니다.");
        }
        return ResultResponse.success("성공적으로 로그아웃되었습니다.");
    }

    /**
     * 제공된 토큰 값으로 사용자 ID를 조회합니다.
     * @param token 인증 토큰 문자열
     * @return 해당 사용자 ID 또는 0 (유효하지 않은 경우)
     */
    private Integer getUserIdFromToken(String token) {
        if (StringUtils.isEmpty(token)) {
            return 0;
        }
        Map<String, String> tokenSearch = new HashMap<>(1);
        tokenSearch.put("token_value", token);
        AuthToken foundToken = authTokenService.findSingleRecord(tokenSearch);
        return (foundToken != null) ? foundToken.getUserId() : 0;
    }

    /**
     * 새로운 사용자 레코드 추가 (오버라이드된 메서드)
     * @param request HTTP 요청 객체
     * @return 추가 결과
     * @throws IOException 요청 본문 읽기 오류 시
     */
    @Override
    @PostMapping("/create_user_record")
    @Transactional
    public Map<String, Object> add(HttpServletRequest request) throws IOException {
        Map<String, Object> newUserData = systemUserService.readRequestBody(request.getReader());
        String rawPassword = String.valueOf(newUserData.get("password_hash"));
        if (rawPassword != null) {
            newUserData.put("password_hash", systemUserService.hashPassword(rawPassword));
        }
        systemUserService.insertRecord(newUserData);
        return ResultResponse.success("사용자 레코드 생성 완료", 1);
    }
}

6.3 MD5 해싱 유틸리티 (Java)


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * MD5 해싱 기능을 제공하는 유틸리티 클래스
 */
public class HashUtil {
    private static final Logger logger = LoggerFactory.getLogger(HashUtil.class);

    // 16진수 문자 배열
    private final static char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
            '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    /**
     * 파일의 MD5 해시값을 계산합니다.
     *
     * @param filePath 해시할 파일의 경로
     * @return MD5 해시 문자열, 오류 발생 시 null
     */
    public static String calculateFileHash(String filePath) {
        FileInputStream fileInput = null;
        try {
            MessageDigest md5Instance = MessageDigest.getInstance("MD5");
            File targetFile = new File(filePath);
            if (!targetFile.exists()) {
                logger.warn("파일이 존재하지 않아 MD5 해시를 계산할 수 없습니다: {}", filePath);
                return "";
            }
            fileInput = new FileInputStream(targetFile);
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = fileInput.read(buffer)) != -1) {
                md5Instance.update(buffer, 0, bytesRead);
            }
            byte[] hashBytes = md5Instance.digest();
            return bytesToHex(hashBytes);
        } catch (NoSuchAlgorithmException e) {
            logger.error("MD5 알고리즘을 찾을 수 없습니다: {}", e.getMessage());
            return null;
        } catch (IOException e) {
            logger.error("파일 읽기 중 오류 발생 (MD5 계산): {} - {}", filePath, e.getMessage());
            return null;
        } finally {
            if (fileInput != null) {
                try {
                    fileInput.close();
                } catch (IOException e) {
                    logger.error("파일 스트림 닫기 오류: {}", e.getMessage());
                }
            }
        }
    }

    /**
     * 바이트 배열을 16진수 문자열로 변환합니다.
     *
     * @param bytes 변환할 바이트 배열
     * @return 16진수 문자열
     */
    private static String bytesToHex(byte[] bytes) {
        StringBuilder hexString = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            hexString.append(HEX_DIGITS[(b >> 4) & 0x0f]);
            hexString.append(HEX_DIGITS[b & 0x0f]);
        }
        return hexString.toString();
    }

    /**
     * 입력 문자열의 MD5 해시값을 계산합니다.
     *
     * @param inputString 해시할 문자열
     * @return MD5 해시 문자열, 입력이 비어있으면 빈 문자열 반환
     */
    public static String getHashString(String inputString) {
        if (StringUtils.isEmpty(inputString)) {
            return "";
        }
        try {
            MessageDigest md5Digest = MessageDigest.getInstance("MD5");
            byte[] hashBytes = md5Digest.digest(inputString.getBytes());
            return bytesToHex(hashBytes);
        } catch (NoSuchAlgorithmException e) {
            logger.error("MD5 알고리즘을 찾을 수 없습니다: {}", e.getMessage());
            return null;
        }
    }
}

7. 시스템 테스트

7.1 테스트 목표

시스템 테스트의 주된 목표는 설계된 웹사이트가 정상적으로 오류 없이 실행되는지, 그리고 기능 모듈이 의도한 대로 작동하는지 검증하는 것입니다. 프로그램 코드에 오류가 없는지 확인하고, 시스템이 사용자 요구사항을 충족하며 안정적인 작동을 보장하는 것이 중요합니다.

7.2 테스트 방법론

본 시스템의 테스트에는 주로 블랙박스 테스트 방법이 사용됩니다. 이는 프로그램 내부 구조나 구현 세부 사항을 알지 못한 채 시스템의 기능적 동작을 외부에서 검증하는 방식입니다. 이를 통해 사용자 관점에서 시스템의 유용성과 안정성을 평가합니다.

  • 모듈 테스트: 각 개별 기능 모듈이 자체적으로 올바르게 작동하는지 확인합니다. 이는 코드 내의 작은 오류나 편차를 식별하고 수정하는 데 중점을 둡니다.
  • 통합 테스트: 여러 모듈이 함께 작동할 때의 상호 작용과 데이터 흐름을 검증합니다. 서브시스템 간의 연결 문제를 찾아내고 해결하는 데 목적이 있습니다.
  • 인수 테스트: 최종 사용자의 관점에서 시스템이 모든 요구사항을 충족하고 기대하는 성능을 발휘하는지 확인합니다. 이 단계에서는 시스템이 실제 운영 환경에 배포될 준비가 되었는지 평가합니다.

7.3 로그인 기능 테스트 시나리오

테스트 대상: 로그인 모듈

테스트 목표: 사용자 이름, 비밀번호를 입력한 후 시스템이 올바르게 인증하고 응답하는지 확인합니다.

테스트 환경: Windows 10, 최신 웹 브라우저 (예: Chrome)

입력 정보: 사용자명, 비밀번호

테스트 절차:

  1. 웹 브라우저를 열고 시스템의 로그인 페이지로 이동합니다.
  2. 로그인 양식에 다양한 사용자명과 비밀번호 조합을 입력하여 테스트합니다.
순서 사용자명 입력 비밀번호 입력 예상 결과
1 testuser1 wrongpass123 "사용자명 또는 비밀번호가 잘못되었습니다." 메시지 표시
2 invaliduser validpass456 "사용자가 존재하지 않습니다." 메시지 표시
3 admin_blog secure_admin_pass 로그인 성공, 관리자 대시보드로 이동

7.4 테스트 결과

온라인 블로그 시스템은 설계 요구사항을 대부분 충족하며, 안정적인 기능과 직관적인 사용자 인터페이스를 갖추고 있습니다. 오류 메시지 처리도 정확하게 이루어집니다. 시스템 테스트 과정에서 발견된 일부 시각적 개선점이나 코드 중복 문제 등은 향후 개선을 통해 보완될 예정입니다. 전반적으로 시스템은 기술적, 운영적, 경제적 측면에서 실행 가능하며, 사용자의 요구를 충족할 수 있어 배포 가치가 있다고 판단됩니다.

태그: SpringBoot Vue.js Element-UI MySQL Web Development

5월 24일 06:19에 게시됨