buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id 'java'
id 'war'
id 'org.springframework.boot' version '3.1.6'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// 기본 라이브러리 ( spring initializer 로 추가하기)
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
implementation 'org.springframework.boot:spring-boot-starter-validation:3.1.1'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.1'
// querydsl 라이브러리
// 참고: https://velog.io/@juhyeon1114/Spring-QueryDsl-gradle-%EC%84%A4%EC%A0%95-Spring-boot-3.0-%EC%9D%B4%EC%83%81
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta" // 기존에는 그냥 jpa, 3 버전 이상은 :jakarta 추가
implementation "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta"
// apt(annotation processing tool), Q도메인 생성. 컴파일 시에만 사용됨
implementation "com.querydsl:querydsl-core"
implementation "com.querydsl:querydsl-sql"
// modelMapper: DTO 와 엔티티 간 변환 처리해주는 라이브러리
implementation "org.modelmapper:modelmapper:3.2.0"
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'junit:junit:4.13.1'
compileOnly("org.projectlombok:lombok", "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta") // 롬복 외에도 querydsl 어노테이션 추가
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
// 기존 롬복 이외도, jakarta 와 querydsl 어노테이션 추가 (spring boot 3.0 이상은 jakarta 대신 jakarta 사용)
annotationProcessor(
"org.projectlombok:lombok",
'javax.persistence:javax.persistence-api:2.2', // 추가
'javax.annotation:javax.annotation-api:1.3.2', // 추가
"jakarta.persistence:jakarta.persistence-api",
"jakarta.annotation:jakarta.annotation-api",
"com.querydsl:querydsl-apt:${queryDslVersion}:jakarta"); // querydsl-apt:jpa -> querydsl-apt:jakarta
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
tasks.named('bootBuildImage') {
builder = 'paketobuildpacks/builder-jammy-base:latest'
}
tasks.named('test') {
useJUnitPlatform()
}
// 프로젝트의 소스 코드 및 리소스 디렉토리 구성
// 개발 시 작성하는 java 파일의 위치 (src/main/java)와 Q도메인이 저장되는 위치(build/generated)를 명시
// 기존 파일과 Q도메인이 gradle 빌드 시 자동 컴파일 되게 함
sourceSets {
main.java.srcDirs = ["$projectDir/src/main/java", "$projectDir/build/generated"]
}
db 연결및 기타 속성 설정
server.port=8085
server.error.path=/error/
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/team16
spring.datasource.username=root
spring.datasource.password=1234
logging.level.package_name=info
logging.level.org.hibernate.type.descriptor.sql=info
logging.level.org.springframework.security=info
spring.servlet.multipart.enabled=false
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=600MB
spring.servlet.multipart.location=c:\\shop
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring boot 3.x.x 버전 부터 multipart 기능을 사용해야할 때 추가해야하는 클래스, 코드
package com.pro06.config;
import jakarta.servlet.MultipartConfigElement;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.unit.DataSize;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
// spring boot 3 이상부터 multipart를 사용할때 이걸 만들어줘야 한다.
@Configuration
public class MultipartConfig {
@Value("${spring.servlet.multipart.location}")
String uploadPath;
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
@Bean
public ModelMapper getMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
.setFieldMatchingEnabled(true)
.setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
.setMatchingStrategy(MatchingStrategies.LOOSE);
return modelMapper;
}
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setLocation(uploadPath);
factory.setMaxRequestSize(DataSize.ofMegabytes(100L));
factory.setMaxFileSize(DataSize.ofMegabytes(100L));
return factory.createMultipartConfig();
}
}
thymeleaf를 사용하는 경우 외부 디렉토리를 지정해줘야 하기에 지정해주는 코드
package com.pro06.config;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
String uploadPath = "file:///c:/shop/";
private final long MAX_AGE_SECS = 3600;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
registry.addResourceHandler("/images/**").addResourceLocations("classpath:/static/images/");
registry.addResourceHandler("/assets/**").addResourceLocations("classpath:/static/assets/");
registry.addResourceHandler("/skydash-admin/**").addResourceLocations("classpath:/static/skydash-admin/");
registry.addResourceHandler("/upload/**").addResourceLocations(uploadPath);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8085")
.allowedMethods("GET","POST","PUT","DELETE","PATCH","OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(MAX_AGE_SECS);
}
// model mapper
@Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
.setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
.setFieldMatchingEnabled(true)
.setMatchingStrategy(MatchingStrategies.STRICT);
return modelMapper;
}
}
jpa에서 테이블을 생성하는 기능을 담당
여러개의 강의를 하나로 묶는 역할을 하는 강좌 테이블
package com.pro06.entity.course;
import com.pro06.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import java.time.LocalDateTime;
// 강좌 테이블
// 여기에 나중에 선생님 관련 컬럼 하나 추가
// validation을 이용해 size, notnull 을 써도 되고 아니면
// column을 이용해 length랑 null able 지정해줘도 됨
@Entity
@Getter
@Table(name="course")
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
// column에 defualt 값을 설정할 때에 밑의 두개를 같이 써줘야 한다.
@DynamicInsert
@DynamicUpdate
public class Course extends BaseEntity {
@Id
@Column(name = "no")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer no; // 강좌 번호
@Comment("작성자(관리자)") // jpa 코멘트 작성 예시
@Column(length = 20, nullable = false)
private String id; // 작성자(관리자)
@Column(length = 10, nullable = false)
private String level; // 학년
@Column(length = 200, nullable = false)
private String title; // 제목
@Column(length = 2000, nullable = false)
private String content; // 내용
@ColumnDefault("0")
private Integer cnt; // 조회수
@ColumnDefault("0")
private Integer peo; // 수강인원
@Column(nullable = false)
private Integer peo_max; // 최대 수강인원
@Column(nullable = true)
private LocalDateTime copendate; // 개강날짜
private Integer copen; // 오픈여부
@ColumnDefault("'n'")
private String deleteYn; // 삭제 여부
public void peoUp(){
this.peo = this.peo + 1;
}
// jpa 에서 수정을 하기위해 선언한 메소드
public void change(String id, String level,
String title, String content, Integer copen) {
this.id = id;
this.level = level;
this.title = title;
this.content = content;
this.copen = copen;
}
// 삭제를 컬럼의 값 형태로 하기위해 선언한 메소드
public void delete(String deleteYn) {
this.deleteYn = deleteYn;
}
}
강의 정보 관련 데이터 저장 테이블
package com.pro06.entity.course;
import com.pro06.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
// 강의 테이블
// 강좌에 속해있으며 여러개의 동영상을 하나로 묶는 역할
@Entity
@Getter
@Table(name="lecture")
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
// column에 defualt 값을 설정할 때에 밑의 두개를 같이 써줘야 한다.
@DynamicInsert
@DynamicUpdate
public class Lecture extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer no; // 강의 번호
@Column(length = 20, nullable = false)
private String id; // 작성자(관리자)
@Column(length = 200, nullable = false)
private String title; // 강의 제목
@Column(length = 2000, nullable = false)
private String content; // 강의 설명
@Column(length = 200, nullable = false)
private String keyword; // 키워드
@ManyToOne(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY)
@JoinColumn(name = "cno", referencedColumnName = "no")
private Course course; // 강좌 번호 외래키 지정
}
강의에 연결된 영상 정보 저장 테이블
package com.pro06.entity.course;
import com.pro06.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
// 강의 영상 테이블
// 강의 영상 정보를 저장
@Entity
@Getter
@Table(name="video")
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
// column에 defualt 값을 설정할 때에 밑의 두개를 같이 써줘야 한다.
@DynamicInsert
@DynamicUpdate
public class Video extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer no; // 영상번호
@Column(nullable = false)
private String savefolder; // 저장경로
@Column(nullable = false)
private String originfile; // 실제 파일 이름
@Column(nullable = false)
private String savefile; // 저장된 파일 이름
@Column(nullable = false)
private Long filesize; // 파일 크기
@ManyToOne(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY)
@JoinColumn(name = "cno", referencedColumnName = "no")
private Course course; // 강좌 번호 외래키 지정
@ManyToOne(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY)
@JoinColumn(name = "lno", referencedColumnName = "no")
private Lecture lecture; // 강의 번호 외래키 지정
}
강의를 다 보고난 후 수강 완료 확인을 위해 검사하는 시험 데이터 테이블
package com.pro06.entity.course;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
@Entity
@Getter
@Table(name="lectest")
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
// column에 defualt 값을 설정할 때에 밑의 두개를 같이 써줘야 한다.
@DynamicInsert
@DynamicUpdate
public class LecTest {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer no; // 시험 번호
// 문제
@Column(nullable = false)
private String exam1;
@Column(nullable = false)
private String exam2;
@Column(nullable = false)
private String exam3;
@Column(nullable = false)
private String exam4;
@Column(nullable = false)
private String exam5;
// 해당 문제의 답안
@Column(nullable = false)
private String answer1;
@Column(nullable = false)
private String answer2;
@Column(nullable = false)
private String answer3;
@Column(nullable = false)
private String answer4;
@Column(nullable = false)
private String answer5;
@ManyToOne(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY)
@JoinColumn(name = "cno", referencedColumnName = "no")
private Course course; // 강좌 번호 외래키 지정
@ManyToOne(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY)
@JoinColumn(name = "lno", referencedColumnName = "no")
private Lecture lecture; // 강의 번호 외래키 지정
}
mvc 패턴을 준수하기 위해 entity 대신 데이터를 주고받기 위해 사용
package com.pro06.dto.course;
import com.pro06.dto.BaseDto;
import jakarta.persistence.Column;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.*;
import java.time.LocalDateTime;
// 강좌 테이블
// 여기에 나중에 선생님 관련 컬럼 하나 추가
// validation을 이용해 size, notnull 을 써도 되고 아니면
// column을 이용해 length랑 null able 지정해줘도 됨
@Getter @Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class CourseDto extends BaseDto {
private Integer no; // 강좌 번호
@Size(max = 20)
@NotBlank
private String id; // 작성자(관리자)
@Size(max = 10)
@NotBlank
private String level; // 학년
@Size(max = 200)
@NotBlank
private String title; // 제목
@Size(max = 2000)
@NotNull
private String content; // 내용
@NotNull
private Integer cnt; // 조회수
@NotNull
private Integer peo; // 수강인원
@NotBlank
private Integer peo_max; // 최대 수강인원
private LocalDateTime copendate; // 개강날짜
private Integer copen; // 오픈여부 open=1, close=0
@NotNull
private String deleteYn; // 삭제 여부
}
package com.pro06.dto.course;
import com.pro06.dto.BaseDto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.*;
// 강의 테이블
// 강좌에 속해있으며 여러개의 동영상을 하나로 묶는 역할
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class LectureDto extends BaseDto {
@NotBlank
private Integer no; // 강의 번호
@Size(max = 20)
@NotBlank
private String id; // 작성자(관리자)
@Size(max = 200)
@NotBlank
private String title; // 강의 제목
@Size(max = 2000)
@NotNull
private String content; // 강의 설명
@Size(max = 200)
@NotNull
private String keyword; // 키워드
@NotNull
private CourseDto course; // 강좌 번호 외래키 지정
}
package com.pro06.dto.course;
import com.pro06.dto.BaseDto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.*;
// 강의 영상 테이블
// 강의 영상 정보를 저장
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class VideoDto extends BaseDto {
@NotBlank
private Integer no; // 영상번호
@Size(max = 255)
@NotBlank
private String savefolder; // 저장경로
@Size(max = 255)
@NotBlank
private String originfile; // 실제 파일 이름
@Size(max = 255)
@NotBlank
private String savefile; // 저장된 파일 이름
@Size(max = 255)
@NotBlank
private Long filesize; // 파일 크기
@NotNull
private CourseDto course; // 강좌 번호 외래키 지정
@NotNull
private LectureDto lecture; // 강의 번호 외래키 지정
}
package com.pro06.dto.course;
import com.pro06.dto.BaseDto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.*;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class LecTestDto extends BaseDto {
@NotBlank
private Integer no; // 시험 번호
// 문제 + 4지선다
@Size(max = 255)
@NotBlank
private String exam1;
@Size(max = 255)
@NotBlank
private String exam2;
@Size(max = 255)
@NotBlank
private String exam3;
@Size(max = 255)
@NotBlank
private String exam4;
@Size(max = 255)
@NotBlank
private String exam5;
// 해당 문제의 답안
@Size(max = 255)
@NotBlank
private String answer1;
@Size(max = 255)
@NotBlank
private String answer2;
@Size(max = 255)
@NotBlank
private String answer3;
@Size(max = 255)
@NotBlank
private String answer4;
@Size(max = 255)
@NotBlank
private String answer5;
@NotNull
private CourseDto course; // 강좌 번호 외래키 지정
@NotNull
private LectureDto lecture; // 강의 번호 외래키 지정
}
여러개의 테이블을 join을 사용하지 않고 자바(백엔드) 부분에서 연결해서 할때 사용
package com.pro06.dto.course;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class LectureVO {
private LectureDto lecture;
private List<VideoDto> fileList;
private LecTestDto lecTest;
private Integer cno;
}
entity로 만들어진 db에서 데이터를 주고받는 기능을 담당
package com.pro06.repository.course;
import com.pro06.entity.course.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CourseRepository extends JpaRepository<Course, Integer> {
// 수강생이 다 차지 않고 삭제 되지 않고 개설된 경우의 강좌만 불러오기
@Query("select c from Course c where c.peo < c.peo_max and c.deleteYn = 'n' and c.copen = 1")
List<Course> courseList();
}
package com.pro06.repository.course;
import com.pro06.entity.course.Lecture;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface LectureRepository extends JpaRepository<Lecture, Integer> {
// cno로 강의 리스트 정보 추출
@Query("select l from Lecture l where l.course.no = :cno")
List<Lecture> lectureCnoList(Integer cno);
// 해당 강좌, 강의 영상 정보 추출
@Query("select l from Lecture l where l.no = :lno and l.course.no = :cno")
Lecture videoList(@Param("cno") Integer cno, @Param("lno") Integer lno);
}
package com.pro06.repository.course;
import com.pro06.dto.course.VideoDto;
import com.pro06.entity.course.Video;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface VideoRepository extends JpaRepository<Video, Integer> {
// 해당 강좌의 해당 강의의 실제 저장된 비디오 이름을 List 형태로 추출
@Query("select v from Video v where v.lecture.no = :lno and v.course.no = :cno")
List<Video> videoList(@Param("cno") Integer cno, @Param("lno") Integer lno);
}
package com.pro06.repository.course;
import com.pro06.entity.course.LecTest;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface LecTestRepository extends JpaRepository<LecTest, Integer> {
// 시험 정보 가져오기
@Query("select lt from LecTest lt where lt.lecture.no = :lno and lt.course.no = :cno")
LecTest getLecTest(@Param("cno") Integer cno, @Param("lno") Integer lno);
}
view에서 요청된 데이터를 controller에서 받아서 repository에 전달하고 다시 controller로 데이터를 전달하는 기능을 담당
package com.pro06.service.course;
import com.pro06.dto.course.CourseDto;
import com.pro06.entity.course.Course;
import com.pro06.repository.course.CourseRepository;
import jakarta.transaction.Transactional;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@Transactional
public class CourseServiceImpl {
@Autowired
private CourseRepository courseRepository;
@Autowired
private ModelMapper modelMapper;
// 강좌 등록
public void courseInsert(CourseDto courseDto) {
Course course = modelMapper.map(courseDto, Course.class);
courseRepository.save(course);
}
// 강좌 수정
public void courseUpdate(CourseDto courseDto) {
Optional<Course> course = courseRepository.findById(courseDto.getNo());
Course res = course.orElseThrow();
res.change(courseDto.getId(), courseDto.getLevel(),
courseDto.getTitle(), courseDto.getContent(),
courseDto.getCopen());
courseRepository.save(res);
}
// 강좌 삭제, 복구
public void couDelRec(CourseDto dto) {
Optional<Course> course = courseRepository.findById(dto.getNo());
Course res = course.orElseThrow();
res.delete(dto.getDeleteYn());
courseRepository.save(res);
}
// 어드민 강좌 목록 불러오기
public List<CourseDto> admCourseList() {
List<Course> lst = courseRepository.findAll();
List<CourseDto> courseList = lst.stream().map(course ->
modelMapper.map(course, CourseDto.class))
.collect(Collectors.toList());
return courseList;
}
// 강좌 목록 불러오기
public List<CourseDto> courseList() {
List<Course> lst = courseRepository.courseList();
List<CourseDto> courseList = lst.stream().map(course ->
modelMapper.map(course, CourseDto.class))
.collect(Collectors.toList());
return courseList;
}
// 강좌 상세 보기
public CourseDto getCourse(Integer no) {
Optional<Course> course = courseRepository.findById(no);
CourseDto courseDto = modelMapper.map(course, CourseDto.class);
return courseDto;
}
// 강좌 수강신청 수강생 +1
public void setCoursePeo(Integer no) {
Optional<Course> course = courseRepository.findById(no);
Course res = course.orElseThrow();
res.peoUp();
courseRepository.save(res);
}
}
package com.pro06.service.course;
import com.pro06.dto.course.*;
import com.pro06.entity.course.*;
import com.pro06.repository.course.*;
import jakarta.transaction.Transactional;
import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@Transactional
@Log4j2
public class LectureServiceImpl {
@Autowired
private CourseRepository courseRepository;
@Autowired
private LectureRepository lectureRepository;
@Autowired
private VideoRepository videoRepository;
@Autowired
private LecTestRepository lecTestRepository;
@Autowired
private LecAnsRepository lecAnsRepository;
@Autowired
private LecQueRepository lecQueRepository;
@Autowired
private ModelMapper modelMapper;
// 강의 정보 + 파일 등록
public void LectureVoInsert(LectureVO vo) throws Exception {
LectureDto lecture = vo.getLecture();
List<VideoDto> fileList = vo.getFileList();
// 강의 정보 등록
Optional<Course> cou = courseRepository.findById(vo.getCno());
CourseDto coudto = modelMapper.map(cou, CourseDto.class);
lecture.setCourse(coudto);
Lecture lec = modelMapper.map(lecture, Lecture.class);
// 저장 및 dto로 변환
Lecture lec2 = lectureRepository.save(lec);
// 이거 안해주면 오류뜸
LectureDto lecDto = modelMapper.map(lec2, LectureDto.class);
log.info("lecture 저장");
// 강의 영상 등록
for(VideoDto video: fileList) {
video.setLecture(lecDto);
video.setCourse(coudto);
Video vdo = modelMapper.map(video, Video.class);
videoRepository.save(vdo);
log.info("video 저장");
}
// 시험 정보 등록
LecTestDto lecTest = vo.getLecTest();
lecTest.setLecture(lecDto);
lecTest.setCourse(coudto);
LecTest lt = modelMapper.map(lecTest, LecTest.class);
lecTestRepository.save(lt);
}
// 해당강의의 강좌 목록 불러오기
public List<LectureDto> lectureCnoList(Integer cno) {
List<Lecture> lst = lectureRepository.lectureCnoList(cno);
List<LectureDto> lectureDtoList = lst.stream().map(lecture ->
modelMapper.map(lecture, LectureDto.class))
.collect(Collectors.toList());
return lectureDtoList;
}
// 시험 정보 가져오기
public LecTestDto getLecTest(Integer cno, Integer lno) {
LecTest opt = lecTestRepository.getLecTest(cno, lno);
if(opt != null) {
return modelMapper.map(opt, LecTestDto.class);
} else {
return null;
}
}
// region lecAns
// 시험 제출 답안 입력, 수정
public void lecAnsInsert(LecAnsDto lecAnsDto) {
LecAns lecAns = modelMapper.map(lecAnsDto, LecAns.class);
lecAnsRepository.save(lecAns);
}
// 제출된 답안 수정
public void lecAnsUpdate(LecAnsDto lecAnsDto) {
Optional<LecAns> lec = lecAnsRepository.getLecAns(lecAnsDto.getCourse().getNo(),
lecAnsDto.getLecture().getNo(), lecAnsDto.getId());
LecAns lecAns = lec.orElseThrow();
lecAns.answerChange(lecAnsDto.getAnswer1(), lecAnsDto.getAnswer2(), lecAnsDto.getAnswer3(),
lecAnsDto.getAnswer4(), lecAnsDto.getAnswer5(), lecAnsDto.getAnsCnt());
lecAnsRepository.save(lecAns);
}
// 답안 정보 추출
public LecAnsDto getLecAns(Integer cno, Integer lno, String id) {
Optional<LecAns> lecAns = lecAnsRepository.getLecAns(cno, lno, id);
log.warn("lecAns.isPresent() : " + lecAns.isPresent());
if(lecAns.isPresent()) { // Optional 에서 null 비교할 때 사용
LecAnsDto lecAnsDto = modelMapper.map(lecAns, LecAnsDto.class);
return lecAnsDto;
} else {
return null;
}
}
// endregion
// region lecQue
// admin 용
// admin 에서 사용할 list
// 모든 질문 목록
public List<LecQueDto> lecQueDtoFindByAll() {
List<LecQue> lst = lecQueRepository.findAll();
List<LecQueDto> dtoList = lst.stream().map(lecQue ->
modelMapper.map(lecQue, LecQueDto.class))
.collect(Collectors.toList());
return dtoList;
}
// user 용
// video 에서 사용할 list
// 해당 강좌, 강의, 영상에서 유저가 질문한 목록
public List<LecQueDto> lecQueList(LecQueDto lecQueDto) {
List<LecQue> lst = lecQueRepository.lecQueList(lecQueDto.getId(), lecQueDto.getPage(),
lecQueDto.getCourse().getNo(), lecQueDto.getLecture().getNo(), "n");
List<LecQueDto> dtoList = lst.stream().map(lecQue ->
modelMapper.map(lecQue, LecQueDto.class))
.collect(Collectors.toList());
return dtoList;
}
// 질문 입력
public LecQueDto lecQueInsert(LecQueDto lecQueDto) {
LecQue lecQue = modelMapper.map(lecQueDto, LecQue.class);
LecQue lec = lecQueRepository.save(lecQue);
LecQueDto dto = modelMapper.map(lec, LecQueDto.class);
return dto;
}
// 질문 보기
public LecQueDto getLecQue(Integer no) {
Optional<LecQue> lecQue = lecQueRepository.findById(no);
LecQueDto dto = modelMapper.map(lecQue, LecQueDto.class);
return dto;
}
// 질문 삭제
public void lecQueDelete(LecQueDto dto) {
Optional<LecQue> lecQue = lecQueRepository.findById(dto.getNo());
LecQue lecQue1 = lecQue.orElseThrow();
lecQue1.delete(dto.getDeleteYn());
lecQueRepository.save(lecQue1);
}
// 질문에 대한 답변 입력, 수정
public void lecQueAnsInsUpd(LecQueDto lecQueDto) {
Optional<LecQue> lecQue = lecQueRepository.findById(lecQueDto.getNo());
LecQue lecQue1 = lecQue.orElseThrow();
lecQue1.answer(lecQueDto.getAns());
lecQueRepository.save(lecQue1);
}
// 질문 삭제 취소
public void lecQueAnsRecover(Integer no) {
Optional<LecQue> lecQue = lecQueRepository.findById(no);
LecQue lecQue1 = lecQue.orElseThrow();
lecQue1.delete("n");
lecQueRepository.save(lecQue1);
}
// endregion
}
package com.pro06.service.course;
import com.pro06.dto.course.LecQueDto;
import com.pro06.dto.course.VideoDto;
import com.pro06.entity.course.Video;
import com.pro06.repository.course.VideoRepository;
import jakarta.transaction.Transactional;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Transactional
public class VideoServiceImpl {
@Autowired
private VideoRepository videoRepository;
@Autowired
private ModelMapper modelMapper;
// 강좌 영상
public List<VideoDto> videoList(Integer cno, Integer lno) {
List<Video> lst = videoRepository.videoList(cno, lno);
List<VideoDto> dtoList = lst.stream().map(video ->
modelMapper.map(video, VideoDto.class))
.collect(Collectors.toList());
return dtoList;
}
}
@Log4j2
@Controller
@Component
@RequestMapping("/admin/*")
public class AdminController {
// 실제 업로드 디렉토리
// thymeleaf 에서는 외부에 지정하여 사용해야 한다.
// jsp와는 다르게 webapp이 없기 때문이다.
// resources는 정적이라 업데이트 되어도 파일을 못 찾기에 서버를 재 시작 해야함
@Value("${spring.servlet.multipart.location}")
String uploadFolder;
@Autowired
private LectureServiceImpl lectureService;
// lecture
// 강의 생성폼 이동
@GetMapping("lectureInsert")
public String lectureInsert(Principal principal, @RequestParam("cno") Integer cno, Model model) {
String id = principal.getName();
model.addAttribute("id", id);
model.addAttribute("cno", cno);
return "admin/lecture/lectureInsert";
}
// spring boot 3 이상부터 이런식으로 사용해야 함
// 강의 생성
// 강의 정보, 강좌 번호, 파일 추출
@PostMapping("lectureInsert")
public String lectureInsertPro(MultipartHttpServletRequest req) throws Exception {
// 입력된 파일목록
List<MultipartFile> files = new ArrayList<>();
// 강의 정보
LectureDto lecture = new LectureDto();
// 여러 파일 반복 저장
List<VideoDto> fileList = new ArrayList<>();
// 파일 저장
for (int i = 0; i < req.getFiles("files").size(); i++) {
files.add(req.getFiles("files").get(i));
}
// 강의정보 저장
CourseDto course = new CourseDto();
Integer cno = Integer.parseInt(req.getParameter("cno"));
course.setNo(cno);
lecture.setCourse(course); // cno 저장
lecture.setId(req.getParameter("id"));
lecture.setTitle(req.getParameter("title"));
lecture.setContent(req.getParameter("content"));
lecture.setKeyword(req.getParameter("keyword"));
// 시험정보 저장
LecTestDto lecTest = new LecTestDto();
lecTest.setExam1(req.getParameter("exam1"));
lecTest.setExam2(req.getParameter("exam2"));
lecTest.setExam3(req.getParameter("exam3"));
lecTest.setExam4(req.getParameter("exam4"));
lecTest.setExam5(req.getParameter("exam5"));
lecTest.setAnswer1(req.getParameter("answer1"));
lecTest.setAnswer2(req.getParameter("answer2"));
lecTest.setAnswer3(req.getParameter("answer3"));
lecTest.setAnswer4(req.getParameter("answer4"));
lecTest.setAnswer5(req.getParameter("answer5"));
// 파일 추출 테스트
for (int i = 0; i < req.getFiles("files").size(); i++) {
log.info("req.getParameter(\"title\") : " + req.getParameter("title"));
log.info("req.getFile(\"files\") : " + req.getFile("files"));
log.info("req.getFile(\"files\").getOriginalFilename() : " +
req.getFiles("files").get(i).getOriginalFilename()); // 실제 파일이름 출력
log.info("req.getFiles(\"files\").get(" + i + ").getBytes() : " +
req.getFiles("files").get(i).getBytes()); // 파일의 용량 출력
log.info("req.getFiles(\"files\").get(" + i + ").getName() : " +
req.getFiles("files").get(i).getName()); // name 속성값 출력
log.info("req.getFiles(\"files\").size() : " +
req.getFiles("files").size()); // 입력된 파일의 개수 출력
}
// 만약 저장 폴더가 없다면 생성
File folder = new File(uploadFolder);
if (!folder.exists()) folder.mkdirs();
// log 출력
log.info("-----------------------------------");
log.info(" 현재 프로젝트 홈 : " + req.getContextPath());
log.info(" 요청 URL : " + req.getServletPath());
log.info(" 파일 저장 경로 : " + uploadFolder);
// 첨부된 파일(MultipartFile) 처리
if (files != null && files.size() > 0) {
for (MultipartFile file : files) {
// 파일 처리 로직 시작
String randomUUID = UUID.randomUUID().toString(); // 파일 이름 중복 방지를 위한 랜덤 UUID 생성
String OriginalFilename = file.getOriginalFilename(); // 실제 파일 이름
String Extension = OriginalFilename.substring(OriginalFilename.lastIndexOf(".")); // 파일 확장자 추출
String saveFileName = randomUUID + Extension; // 저장할 파일 이름 생성
// 저장위치, 실제파일이름, 저장될 파일이름, 파일크기 정보를 저장
VideoDto data = new VideoDto();
data.setSavefolder(uploadFolder);
data.setOriginfile(file.getOriginalFilename());
data.setSavefile(saveFileName);
data.setFilesize(file.getSize());
fileList.add(data);
// 파일 저장
File saveFile = new File(uploadFolder, saveFileName);
try {
file.transferTo(saveFile); // 실제 upload 위치에 파일 저장
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
// 예외 처리
}
}
}
// VO를 통해 db에 저장
LectureVO lectureVO = new LectureVO();
lectureVO.setLecture(lecture);
lectureVO.setFileList(fileList);
lectureVO.setLecTest(lecTest);
lectureVO.setCno(cno);
lectureService.LectureVoInsert(lectureVO); // 강의와 비디오를 같이 저장
return "redirect:/admin/courseDetail?no=" + cno;
}
}
사용자가 실제로 보는 곳, 나는 thymeleaf 사용
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>강의등록</title>
<style>
.file-list {
height: 200px;
width: 400px;
overflow: auto;
border: 1px solid #989898;
padding: 10px;
}
.file-list .filebox p {
font-size: 14px;
margin-top: 10px;
display: inline-block;
}
.file-list .filebox .delete i{
color: #ff5353;
margin-left: 5px;
cursor: pointer;
}
</style>
<th:block th:replace="/include/adminhead :: head"></th:block>
</head>
<body>
<div class="container-scroller">
<div class="container-fluid page-body-wrapper">
<th:block th:replace="/include/adminheader :: header"></th:block>
<div class="main-panel">
<div class="content-wrapper">
<h1>관리자페이지</h1>
<h2>강의 등록</h2>
<div class="card p-3">
<form method="post" class="text-center mb-3 was-validated" enctype="multipart/form-data">
<div class="form-group">
<label for="id" class="form-label">아이디</label>
<input type="hidden" name="cno" id="cno" th:value="${cno}" readonly>
<input type="text" name="id" id="id" class="form-control" th:value="${id}" readonly>
</div>
<div class="form-group">
<label for="title" class="form-label">제목</label>
<input type="text" name="title" id="title" class="form-control"
pattern="^[ㄱ-ㅎ가-힣0-9a-zA-Z\s]+$" maxlength="100" required>
<div class="valid-feedback">
형식에 알맞습니다.
</div>
<div class="invalid-feedback">
한글, 숫자, 영어대소문자만 가능합니다.
</div>
</div>
<div class="form-group">
<label for="keyword" class="form-label">키워드</label>
<input type="text" name="keyword" id="keyword" class="form-control"
pattern="^[ㄱ-ㅎ가-힣0-9a-zA-Z\s]+$" maxlength="100" required>
<div class="valid-feedback">
형식에 알맞습니다.
</div>
<div class="invalid-feedback">
한글, 숫자, 영어대소문자만 가능합니다.
</div>
</div>
<div class="form-group">
<label for="content" class="form-label">내용</label>
<input type="text" name="content" id="content" class="form-control"
pattern="^[ㄱ-ㅎ가-힣0-9a-zA-Z\s]+$" maxlength="1000" required>
<div class="valid-feedback">
형식에 알맞습니다.
</div>
<div class="invalid-feedback">
한글, 숫자, 영어대소문자만 가능합니다.
</div>
</div>
<hr style="border-top: 1px solid #333">
<div class="form-group">
<label for="file" class="form-label">강의영상</label>
<input type="file" id="file" class="form-control" name="file" onchange="addFile(this);" multiple accept="video/mp4"/>
<div class="file-list"></div>
</div>
<hr style="border-top: 1px solid #333">
<div class="form-group">
<!-- 문제, 답 5개 씩 만듬 -->
<th:block th:each="i : ${#numbers.sequence(1, 5)}">
<div>
<div class="form-group">
<label th:for="'exam' + ${i}" class="form-label" th:text="'문제 ' + ${i}"></label>
<input type="text" class="exam form-control" th:name="'exam' + ${i}"
th:id="'exam' + ${i}" pattern="^[ㄱ-ㅎ가-힣a-zA-Z]+$" maxlength="100" required>
<div class="valid-feedback">
형식에 알맞습니다.
</div>
<div class="invalid-feedback">
한글, 숫자, 영어대소문자만 가능합니다.
</div>
</div>
<br>
<div class="form-group">
<label th:for="'answer' + ${i}" class="form-label" th:text="'답 ' + ${i}"></label>
<input type="text" class="exam form-control" th:name="'answer' + ${i}"
th:id="'answer' + ${i}" pattern="^[ㄱ-ㅎ가-힣a-zA-Z]+$" maxlength="100" required>
<div class="valid-feedback">
형식에 알맞습니다.
</div>
<div class="invalid-feedback">
한글, 숫자, 영어대소문자만 가능합니다.
</div>
</div>
</div>
<hr style="border-top: 1px solid #333"><br>
</th:block>
</div>
<div>
<input type="button" class="btn btn-primary" th:onclick="return submitForm()" value="등록">
<input type="reset" class="btn btn-danger" value="초기화">
</div>
</form>
</div>
</div>
<footer class="footer">
<div class="d-sm-flex justify-content-center justify-content-sm-between">
<span class="text-muted text-center text-sm-left d-block d-sm-inline-block">Copyright © 2021. Premium <a href="https://www.bootstrapdash.com/" target="_blank">Bootstrap admin template</a> from BootstrapDash. All rights reserved.</span>
<span class="float-none float-sm-right d-block mt-1 mt-sm-0 text-center">Hand-crafted & made with <i class="ti-heart text-danger ml-1"></i></span>
</div>
</footer>
</div>
</div>
</div>
<script th:inline="javascript">
let fileNo = 0;
let filesArr = new Array();
/*<![CDATA[*/
let cno = [[${cno}]];
/*]]>*/
/* 첨부파일 추가 */
function addFile(obj){
let maxFileCnt = 5; // 첨부파일 최대 개수
let attFileCnt = document.querySelectorAll('.filebox').length; // 기존 추가된 첨부파일 개수
let remainFileCnt = maxFileCnt - attFileCnt; // 추가로 첨부가능한 개수
let curFileCnt = obj.files.length; // 현재 선택된 첨부파일 개수
// 첨부파일 개수 확인
if (curFileCnt > remainFileCnt) {
alert("첨부파일은 최대 " + maxFileCnt + "개 까지 첨부 가능합니다.");
} else {
for (const file of obj.files) {
// 첨부파일 검증
if (validation(file)) {
// 파일 배열에 담기
var reader = new FileReader();
reader.onload = function () {
filesArr.push(file);
};
reader.readAsDataURL(file);
console.log(file);
// 목록 추가
let htmlData = '';
htmlData += '<div id="file' + fileNo + '" class="filebox">';
htmlData += ' <p class="name">' + file.name + '</p>';
htmlData += ' <a class="delete"token operator">+ fileNo + ');"><i>삭제</i></a>';
htmlData += '</div>';
$('.file-list').append(htmlData);
fileNo++;
} else {
continue;
}
}
}
// 초기화
document.querySelector("input[type=file]").value = "";
}
/* 첨부파일 검증 */
function validation(obj){
const fileTypes = ['video/mp4'];
if (obj.name.length > 100) {
alert("파일명이 100자 이상인 파일은 제외되었습니다.");
return false;
} else if (obj.size > (100 * 1024 * 1024)) {
alert("최대 파일 용량인 100MB를 초과한 파일은 제외되었습니다.");
return false;
} else if (obj.name.lastIndexOf('.') == -1) {
alert("확장자가 없는 파일은 제외되었습니다.");
return false;
} else if (!fileTypes.includes(obj.type)) {
alert("첨부가 불가능한 파일은 제외되었습니다.");
return false;
} else {
return true;
}
}
/* 첨부파일 삭제 */
function deleteFile(num) {
if(!confirm('해당 파일을 삭제하시겠습니까?')) {
return false;
}
document.querySelector("#file" + num).remove();
filesArr[num].is_delete = true;
}
/* 폼 전송 */
function submitForm() {
let title = $("#title").val();
let keyword = $("#keyword").val();
let content = $("#content").val();
let exam = $(".exam").length;
if(title === '') {
alert('제목을 입력하세요');
$('#title').focus();
return false;
}
if(keyword === '') {
alert('키워드를 입력하세요');
return false;
}
if(content === '') {
alert('설명을 입력하세요')
return false;
}
// 동영상 체크
let attFileCnt = document.querySelectorAll('.filebox').length; // 기존 추가된 첨부파일 개수
if(attFileCnt === 0) { // 추가된 파일이 없으면 submit 실패
alert('동영상 파일이 최소 1개이상 있어야 합니다.')
return false;
}
// 시험 정보 체크
for(var i=0; i<exam; i++) {
let value = $(".exam").eq(i).val()
if(value === '') {
alert('시험에 입력되지 않은 곳이 존재합니다.')
return false;
}
}
// 폼데이터 담기
let form = document.querySelector("form");
let formData = new FormData(form);
for (var i = 0; i < filesArr.length; i++) {
// 삭제되지 않은 파일만 폼데이터에 담기
if (!filesArr[i].is_delete) {
formData.append("files", filesArr[i]);
}
}
// ajax로 form 데이터 전송
$.ajax({
method: 'POST',
url: '/admin/lectureInsert',
data: formData, // 필수
cache: false,
enctype: 'multipart/form-data', // 필수
processData: false, // 필수
contentType: false, // 필수
success: function () {
console.log('강의 업로드 성공');
location.href = '/admin/courseDetail?no=' + cno;
},
error: function () {
alert('강의 업로드 실패');
return false;
}
})
}
</script>
</body>
</html>