자바풀스택 과정/자바 풀 스택 : 수업내용정리

자바 풀 스택 2/25 오전 기록 062-1

파티피플지선 2025. 2. 25. 13:28

9:23 경 학원 도착
 
 
 
 
 
<9:30 1교시>
일단 수업 따라가려고 선생님이 작성한거 가져옴..

 
 
자바스크립트에서
let result="kim";     let result=name+"gura";     이 두줄을 백틱을 사용해서 let result=` ${name}gura`; 로 합쳐서 쓸 수 있는 것처럼 타임 리프에서는 | |사이에 있는 내용이 하나의 문자열로 인식되어 타임리프언어와 자바스크립트언어를 같이 사용할 수 있다. 
즉,  | 번호 : ${ num } |, [[ | 번호 : ${num} | ]] 이런식으로 작성하면 오류 없이 작성할 수 있다.
링크도 마찬가지이다. @{|study?num=${num}&name=${name}|} 이런식으로 작성하면 타임리프가 참조할 수 있는 형태로 작성할 수 있다.
이거보다 불편한 형태지만 @{'study?num='+${num}+'&name='+${name}} 형태로 싱글따옴표를 사용해서 작성할수도 있긴 하다.
그리고 get 방식 파라미터에 한해서는 @{/study(num=${num}, name=${name})}이란 형태로 작성할 수도 있다.

 
 
 
당연히 ne 대신 != 도 된다.

 
 
페이지네이션 클래스 어떻게 만들었나 궁금해서 찾아봄

더보기

 

(출처 https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.css)

.pagination {
  --bs-pagination-padding-x: 0.75rem;
  --bs-pagination-padding-y: 0.375rem;
  --bs-pagination-font-size: 1rem;
  --bs-pagination-color: var(--bs-link-color);
  --bs-pagination-bg: var(--bs-body-bg);
  --bs-pagination-border-width: var(--bs-border-width);
  --bs-pagination-border-color: var(--bs-border-color);
  --bs-pagination-border-radius: var(--bs-border-radius);
  --bs-pagination-hover-color: var(--bs-link-hover-color);
  --bs-pagination-hover-bg: var(--bs-tertiary-bg);
  --bs-pagination-hover-border-color: var(--bs-border-color);
  --bs-pagination-focus-color: var(--bs-link-hover-color);
  --bs-pagination-focus-bg: var(--bs-secondary-bg);
  --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
  --bs-pagination-active-color: #fff;
  --bs-pagination-active-bg: #0d6efd;
  --bs-pagination-active-border-color: #0d6efd;
  --bs-pagination-disabled-color: var(--bs-secondary-color);
  --bs-pagination-disabled-bg: var(--bs-secondary-bg);
  --bs-pagination-disabled-border-color: var(--bs-border-color);
  display: flex;
  padding-left: 0;
  list-style: none;
}

.page-link {
  position: relative;
  display: block;
  padding: var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);
  font-size: var(--bs-pagination-font-size);
  color: var(--bs-pagination-color);
  text-decoration: none;
  background-color: var(--bs-pagination-bg);
  border: var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);
  transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
  .page-link {
    transition: none;
  }
}
.page-link:hover {
  z-index: 2;
  color: var(--bs-pagination-hover-color);
  background-color: var(--bs-pagination-hover-bg);
  border-color: var(--bs-pagination-hover-border-color);
}
.page-link:focus {
  z-index: 3;
  color: var(--bs-pagination-focus-color);
  background-color: var(--bs-pagination-focus-bg);
  outline: 0;
  box-shadow: var(--bs-pagination-focus-box-shadow);
}
.page-link.active, .active > .page-link {
  z-index: 3;
  color: var(--bs-pagination-active-color);
  background-color: var(--bs-pagination-active-bg);
  border-color: var(--bs-pagination-active-border-color);
}
.page-link.disabled, .disabled > .page-link {
  color: var(--bs-pagination-disabled-color);
  pointer-events: none;
  background-color: var(--bs-pagination-disabled-bg);
  border-color: var(--bs-pagination-disabled-border-color);
}

.page-item:not(:first-child) .page-link {
  margin-left: calc(var(--bs-border-width) * -1);
}
.page-item:first-child .page-link {
  border-top-left-radius: var(--bs-pagination-border-radius);
  border-bottom-left-radius: var(--bs-pagination-border-radius);
}
.page-item:last-child .page-link {
  border-top-right-radius: var(--bs-pagination-border-radius);
  border-bottom-right-radius: var(--bs-pagination-border-radius);
}

.pagination-lg {
  --bs-pagination-padding-x: 1.5rem;
  --bs-pagination-padding-y: 0.75rem;
  --bs-pagination-font-size: 1.25rem;
  --bs-pagination-border-radius: var(--bs-border-radius-lg);
}

.pagination-sm {
  --bs-pagination-padding-x: 0.5rem;
  --bs-pagination-padding-y: 0.25rem;
  --bs-pagination-font-size: 0.875rem;
  --bs-pagination-border-radius: var(--bs-border-radius-sm);
}

 
아래 코드에 대한 챗지피티의 설명

이 코드에서 #numbers와 append는 Thymeleaf 템플릿 엔진의 기능을 사용하여 HTML을 동적으로 생성하는 부분입니다. 각 부분을 설명하겠습니다.
1. #numbers.sequence(dto.startPageNum, dto.endPageNum)
#numbers는 Thymeleaf의 내장 유틸리티 객체입니다. 이 객체는 숫자와 관련된 여러 유용한 기능을 제공합니다.sequence는 #numbers의 메서드 중 하나로, 특정 범위의 숫자 리스트를 생성합니다. 이 메서드는 두 개의 인자를 받습니다:dto.startPageNum: 페이지네이션 시작 번호dto.endPageNum: 페이지네이션 끝 번호#numbers.sequence(dto.startPageNum, dto.endPageNum)은 dto.startPageNum부터 dto.endPageNum까지의 숫자 범위를 생성하여 반환합니다. 예를 들어, dto.startPageNum = 1이고 dto.endPageNum = 5라면, #numbers.sequence는 [1, 2, 3, 4, 5]를 반환합니다.
2. th:classappend="${i} == ${dto.pageNum} ? 'active'"
th:classappend는 주어진 조건이 참일 때, 해당 HTML 요소에 추가 클래스를 동적으로 붙여주는 Thymeleaf 속성입니다.이 속성은 i == dto.pageNum ? 'active'라는 조건을 사용하고 있습니다.이 조건은 현재 페이지 번호(dto.pageNum)와 i가 같으면 active 클래스를 추가하는 방식입니다.예를 들어, i가 dto.pageNum과 같으면, <li> 태그에 class="page-item active"가 추가됩니다.active 클래스를 추가함으로써, 현재 페이지를 강조 표시하는 효과를 낼 수 있습니다.
결합된 흐름
#numbers.sequence(dto.startPageNum, dto.endPageNum)는 페이지 번호 범위를 생성합니다.각 페이지 번호에 대해 th:classappend는 해당 번호가 현재 페이지(dto.pageNum)와 일치하는지 확인하고, 일치하면 그 페이지 번호에 active 클래스를 추가합니다.
결과적으로 이 코드는 페이지네이션을 구현하는데 사용됩니다. 사용자가 현재 보고 있는 페이지를 하이라이트하기 위해 active 클래스를 동적으로 적용하며, 페이지 번호 링크를 생성합니다.

 
 
타임리프에 셀렉티드 속성 : true면 selected가 추가되고, false면 추가되지 않아서 선택되지 않음

 
문자열이 비었는지 확인하여 그 조건을 만족할 때 p 요소를 출력하겠다는 의미의 코드

 
챗지피티에게도 물어봄

Thymeleaf에서 #strings는 문자열과 관련된 작업을 수행할 수 있는 내장 유틸리티 객체입니다. 이 객체는 문자열을 다루는 여러 가지 유용한 메서드를 제공합니다.
#strings.isEmpty(dto.keyword)
이 부분은 #strings.isEmpty 메서드를 사용하여 문자열이 비어 있는지를 확인하는 코드입니다.
#strings.isEmpty(dto.keyword)는 dto.keyword가 빈 문자열인지, 즉 값이 없거나 공백인지를 확인합니다.만약 dto.keyword가 비어 있지 않으면 (not을 사용하여 반전시킴), 그 결과는 true가 되어, <p> 태그가 화면에 렌더링됩니다.반대로, dto.keyword가 비어 있거나 null이라면 #strings.isEmpty(dto.keyword)는 true가 되어, th:if 조건이 만족되지 않아 <p> 태그는 렌더링되지 않습니다.
예시
dto.keyword = "hello"라면, #strings.isEmpty("hello")는 false가 되어, <p> 태그가 출력됩니다.dto.keyword = ""라면, #strings.isEmpty("")는 true가 되어, <p> 태그는 출력되지 않습니다.
결론
#strings.isEmpty(dto.keyword)는 주어진 dto.keyword가 비어 있지 않은지 검사하고, 비어 있지 않으면 해당 검색 결과에 대한 메시지를 출력하는 데 사용됩니다.

 
이제는 새 글 작성 페이지를 만들어야 하는데, 우리가 전에 사용했던 스마트 에디터는 jsp에 맞춰진 구조이다.
일단 SmartEditor 폴더 가져와서 static 폴더에 붙여넣기

 
스마트에디터 사용 설정은 2교시에 이어서.
 
 
<10:30 2교시>

스마트에디터에서 파일, 특히 이미지를 업로드했을 때 파일을 업로드해주는 기능을 하던 jsp 페이지가 webapp에 있지도 않고, static 폴더에 있으면서 jsp 관련 동작을 설정하지도 않았어서 지금으로선 동작을 안하고 있다.
그래서 이미지 업로드 요청을 받아줄 컨트롤러가 필요하다.
 

 
새로운 클래스 : SmartEditorController 만들기

 
스마트 에디터 컨트롤러 만들기

더보기
package com.example.spring10.controller;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import jakarta.servlet.http.HttpServletRequest;


/*
 * 	이 컨트롤러가 정상동작 하기 위해서는 
 * 
 *  1. SmartEditor 폴더를 static 폴더에 붙여 넣기 하고 
 *  2. /upload/{imageName}  요청에 대해서 이미지를 출력해주는 ImageController 가 있어야하고 
 *  3. SmartEditor/photo_uploader/popup/attach_photo.js  에 있는 코드를
 *  
 *  아래와 같이 수정해야 한다. 
 *  
 *      function html5Upload() {	
    	var tempFile,
    		sUploadURL;
    	
    	//sUploadURL= 'file_uploader_html5.jsp'; 	//upload URL
    	//jsp 페이지에 요청하던 요청경로를 SmartEditorController 에 요청을 하도록 수정한다.
    	sUploadURL="/spring10/editor_upload";
    	
    여기서 /spring10 는  context path 이기 때문에 상황에 맞게 변경해야 한다. 
 *  
 */

@Controller
public class SmartEditorController {
	
	//업로드된 이미지를 저장할 서버의 경로 읽어오기 
	@Value("${file.location}")
	private String fileLocation; 
	
	//ajax 업로드 요청에 대해 응답을 하는 컨트롤러 메소드
	@PostMapping("/editor_upload")
	@ResponseBody
	public String upload(HttpServletRequest request) throws IOException {
		
	    //파일정보
	    String sFileInfo = "";
	    //파일명을 받는다 - 일반 원본파일명
	    String filename = request.getHeader("file-name");
	    //파일 확장자
	    String filename_ext = filename.substring(filename.lastIndexOf(".") + 1);
	    //확장자를소문자로 변경
	    filename_ext = filename_ext.toLowerCase();
	 
	    //이미지 검증 배열변수
	    String[] allow_file = { "jpg", "png", "bmp", "gif" };
	 
	    //돌리면서 확장자가 이미지인지 
	    int cnt = 0;
	    for (int i = 0; i < allow_file.length; i++) {
	        if (filename_ext.equals(allow_file[i])) {
	            cnt++;
	        }
	    }
	 
	    //이미지가 아님
	    if (cnt == 0) {
	        /*
	         * 이미지가 아니면 클라이언트에게
	         * 
	         * NOTALLOW_파일명 
	         * 
	         * 을 응답한다.
	         */
	    	//out.println("NOTALLOW_" + filename);
	    	return "NOTALLOW_" + filename;
	    } else {
	        //이미지이므로 신규 파일로 디렉토리 설정 및 업로드   
	        
	        //파일 기본경로 _ 상세경로
	        String filePath = fileLocation + File.separator;
	        File file = new File(filePath);
	        if (!file.exists()) {
	            file.mkdirs();
	        }
	        String realFileNm = "";
	        SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss");
	        String today = formatter.format(new java.util.Date());
	        realFileNm = today + UUID.randomUUID().toString() + filename.substring(filename.lastIndexOf("."));
	        String rlFileNm = filePath + realFileNm;
	        ///////////////// 서버에 파일쓰기 ///////////////// 
	        InputStream is = request.getInputStream();
	        OutputStream os = new FileOutputStream(rlFileNm);
	        int numRead;
	        byte b[] = new byte[Integer.parseInt(request.getHeader("file-size"))];
	        while ((numRead = is.read(b, 0, b.length)) != -1) {
	            os.write(b, 0, numRead);
	        }
	        if (is != null) {
	            is.close();
	        }
	        os.flush();
	        os.close();
	        ///////////////// 서버에 파일쓰기 /////////////////
	 		String contextPath=request.getContextPath();
	        // 업로드된 이미지의 정보를 클라이언트에게 출력
	        sFileInfo += "&bNewLine=true";    
	        sFileInfo += "&sFileName=" + filename;    
	        sFileInfo += "&sFileURL="+contextPath+"/upload/"+realFileNm;
	        //out.println(sFileInfo);
	        return sFileInfo;
	    }
	}
}

 
 
 
시큐리티가 iframe의 동작도 막고 있어서 sercuirtyconfig에 코드 추가할거임

 
 
iframe 을 사용할수 있도록 설정하는 코드(default 값은 사용 불가, SmartEditor에서 필요로함)
얘를 설정하지 않으면 iframe 에서 페이지를 띄울 수 없다.

 
스마트 에디터는 iframe이라는 페이지 안의 페이지를 띄워서 이미지를 보여주는데, 스프링 시큐리티는 iframe이 새로운 창을 띄우려고 src를 가져오는것 조차 막아버려서 이 기능을 해제하는 코드라고 이해하면 됨.
 
챗지피티한테도 물어봄

스프링 부트에서 iframe을 사용할 수 있도록 설정하는 이유는 보안과 관련이 있습니다. 기본적으로, 스프링 부트는 iframe을 비활성화하도록 설정되어 있습니다. 그 이유는 주로 클릭재킹(clickjacking) 공격을 방어하기 위해서입니다. 클릭재킹 공격은 사용자가 자신이 의도하지 않은 클릭을 하도록 유도하는 공격 방식으로, iframe을 악용하여 사용자를 속일 수 있습니다.

1. X-Frame-Options 헤더

스프링 부트는 기본적으로 X-Frame-Options HTTP 헤더를 설정하여 페이지를 iframe에서 렌더링하지 못하도록 막습니다. 이 헤더의 기본 값은 DENY 또는 SAMEORIGIN으로 설정되어 있습니다:

  • DENY: 페이지가 어떤 iframe에도 표시되지 않도록 차단합니다.
  • SAMEORIGIN: 페이지가 동일 출처에서만 iframe에 표시될 수 있도록 합니다.

즉, 기본적으로 다른 도메인에서 제공되는 페이지를 iframe에 넣을 수 없으며, 동일 출처에서만 iframe을 사용할 수 있습니다.

2. CSRF(Cross-Site Request Forgery) 보호

또한, 스프링 시큐리티는 기본적으로 CSRF(Cross-Site Request Forgery) 보호를 활성화합니다. CSRF 공격을 막기 위해 기본적으로 폼 제출 시 CSRF 토큰을 요구합니다. 이를 비활성화하는 설정도 가능합니다. SmartEditor나 다른 특정 웹 애플리케이션에서 CSRF 보호를 끄고 사용해야 할 때가 있을 수 있습니다.

3. SmartEditor에서 iframe 필요

SmartEditor는 네이버의 스마트에디터를 포함하는 에디터로, 사용자가 편집한 내용을 미리보기 위해 iframe을 필요로 할 수 있습니다. 이 경우, 스프링 부트의 기본 보안 설정이 방해가 되므로 이를 해제해야 합니다.

4. 해결 방법

스프링 시큐리티에서 iframe을 허용하려면 X-Frame-Options를 SAMEORIGIN 또는 ALLOW-FROM 등으로 설정해야 하고, CSRF 보호를 비활성화해야 할 수도 있습니다.

위 코드에서 iframe 사용을 허용하고 CSRF를 비활성화하는 방법은 다음과 같습니다:

java
복사
httpSecurity .headers(headers -> headers .frameOptions(frameOptions -> frameOptions.sameOrigin()) // SAMEORIGIN으로 설정하여 동일 출처에서만 iframe을 사용할 수 있도록 설정 ) .csrf(csrf -> csrf.disable()); // CSRF 보호 비활성화 (SmartEditor에서는 이를 필요로 할 수 있음)
  • frameOptions.sameOrigin(): 이 설정은 iframe을 동일 출처에서만 사용할 수 있게 합니다.
  • csrf.disable(): CSRF 보호를 비활성화하여, SmartEditor와 같은 특정 애플리케이션에서 요구하는 동작을 가능하게 합니다.

5. 주의 사항

  • 보안상 고려사항: iframe을 사용할 때는 보안에 특히 유의해야 합니다. 특히, 외부 출처에서 iframe을 포함시키거나 CSRF 보호를 비활성화하는 경우, 악성 공격에 취약해질 수 있습니다.
  • 정확한 요구사항 파악: SmartEditor나 특정 에디터가 iframe을 필요로 하는 이유를 이해하고, 해당 요구사항에 맞는 보안 설정을 하는 것이 중요합니다.

 
 
저장도 안 했는데 어떻게 글 번호를 가져오지!
저장하고 글 번호를 리턴하는 메소드가 필요함.
글을 저장하는 시점에 글번호를 넣는게 아니라 글 번호를 미리 얻어내서 저장해야 한다는 이야기.
 

 
글 번호를 미리 읽어와서 넣어줄거니까 insert에서도 #{num} 으로 바꿔줌

 

 
 
에러 나서 수정

 
아직 새글을 자세히 보는 것을 다루는 view 페이지를 만든 것은 아니어서 작성한 내용이 보이는 것은 아님.

 
 
PostService 인터페이스에서 글 하나의 정보를 가져오는 메소드 추가

 
Dao가 없어서 Dao로 가서 메소드 추가

 
Dao에 추가된 메소드와 관련해서 Mapper 작성

 
 
 
<11:30 3교시>
글을 저장했을 때 리다이렉트되면서 글 저장 성공 메시지를 전달할 RedirectAttributes도 함께 전달해서 글 작성이 성공적으로 이루어졌음을 보여주기로 함.

 
 
 
단순히 해당 글 정보만 가져올 게 아니라 이전글 정보, 다음글 정보도 어느정도는 가져와야 하고, 검색어 조건이 있을 때의 경우도 고려해야 한다.

 
 

 
 
키워드가 있을 때 이전글 글번호와 다음글 글번호까지 담아주기

 
서비스로 이동해서 완성해주기

 
 
Post컨트롤러로 가서

 
 
 view페이지로 가서 작성

 
 
 

 
 
글 내용이 두번 나오는데 그 이유는 글 내용을 작성하는 코드를 서로 다르게 두번 작성했으니까임.

 

 
 
 
글작성자와 로그인한 사람이 일치한다면 버튼을 띄워준 부분을 동적으로 코딩하기.

 
 
 
그리고 이건 아직 수업시간에 안했는데 확인해보라는 주석이 있어서 확인해봄.

<12:30 4교시>
오후엔 댓글 기능 나간다고 함. 종이에 서로 연관 관계랑 작성할 때 놓치면 안되는 것 정리해보려고 함.