8.  수정 - 제목, 내용만

  • DAO에 수정하는 updateOne() 메서드 작성
  • 테스트 코드 작성 후 테스트
  • Service 작업
  • 컨트롤러에 modify 추가
  • modify.jsp 작성
public interface BoardMapper {

    void insert(BoardVO boardVO); // DB 저장하는 메소드

    List<BoardVO> selectAll();

    BoardVO selectOne(Integer no);

    void updateHit(Integer no);

    void updateOne(BoardVO boardVO);
}
    <update id="updateOne">
        UPDATE tbl_board SET title = #{title}, content= #{content} WHERE no = #{no}
    </update>
    @Test
    public void updateOne() {
        Integer no = 8;
        BoardVO boardVO = BoardVO.builder()
                .no(no)
                .title("수정")
                .content("수정된 내용...")
                .build();

        boardMapper.updateOne(boardVO);
        log.info(boardMapper.selectOne(no));
    }

public interface BoardService {

    void register(BoardDTO boardDTO);

    List<BoardDTO> getAll();

    BoardDTO getOne(Integer no);

    void updateOne(BoardDTO boardDTO);
}
    @Override
    public void updateOne(BoardDTO boardDTO) {
        log.info("update...");

        BoardVO boardVO = modelMapper.map(boardDTO, BoardVO.class);
        log.info(boardVO);
        boardMapper.updateOne(boardVO);

    }
    @PostMapping("/modify")
    public String modify(@Valid BoardDTO boardDTO,
                         BindingResult bindingResult,
                         RedirectAttributes redirectAttributes) {

        if (bindingResult.hasErrors()) {
            // 유효성 검사 결과 에러가 있으면 수정 페이지로 돌아감
            log.info("has error");

            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            redirectAttributes.addAttribute("no", boardDTO.getNo());
            return "redirect:/board/modify";
        }
        boardService.updateOne(boardDTO);
        return "redirect:/board/read?no=" + boardDTO.getNo();
    }
<div class="card-body">
    <form action="/board/modify" method="post">
    <input type="hidden" name="no" value="${dto.no}">
    <br>
    <div class="input-group">
        <span class="input-group-text">Title</span>
            <input type="text" name="title" class="form-control" value="${dto.title}">
    </div><br>
    <div class="input-group">
        <span class="input-group-text">Content</span>
        <textarea name="content" class="form-control">${dto.content}</textarea>
    </div><br>
    <div class="input-group">
        <span class="input-group-text">Writer</span>
        <input type="text" name="writer" class="form-control" value="${dto.writer}" readonly>
    </div><br>
    <div class="input-group">
        <span class="input-group-text">Date</span>
        <input type="date" name="dueDate" class="form-control" value="${dto.addDate}" readonly>
    </div><br>
    <div class="my-4">
        <div class="float-end">
            <button type="button" class="btn btn-danger">Remove</button>
            <button type="button" class="btn btn-primary">Modify</button>
            <button type="button" class="btn btn-secondary">List</button>
        </div>
    </div>
    </form>
    
    <script>
        const serverValidResult = {};
        <c:forEach items="${errors}" var="error">
            serverValidResult['${error.getField()}'] = '${error.defaultMessage}';
        </c:forEach>
        console.log(serverValidResult);
    </script>
                    
    <script>
        const frmModify = document.querySelector('form');
        document.querySelector('.btn-danger').addEventListener('click', function () {
            frmModify.action = '/board/remove';
            frmModify.method = 'post';
            frmModify.submit();
        });
        document.querySelector('.btn-primary').addEventListener('click', function () {
            frmModify.action = '/board/modify';
            frmModify.method = 'post';
            frmModify.submit();
        });
        document.querySelector('.btn-secondary').addEventListener('click', function (e) {
            self.location = `/board/list?${boardDTO.link}`;
        });
    </script>
</div>

 


 

9.  삭제

  • DAO에 삭제하는 deleteOne() 메서드 작성
  • 테스트 코드 작성 후 테스트
  • Service 작업
  • 컨트롤러에 remove 추가
public interface BoardMapper {

    void insert(BoardVO boardVO); // DB 저장하는 메소드

    List<BoardVO> selectAll();

    BoardVO selectOne(Integer no);

    void updateHit(Integer no);

    void updateOne(BoardVO boardVO);

    void deleteOne(Integer no);
}
    <delete id="deleteOne">
        DELETE FROM tbl_board WHERE no = #{no}
    </delete>
    @Test
    public void deleteOne() {
        Integer no = 8;
        BoardVO boardVO = boardMapper.selectOne(no);
        log.info(boardVO);

        boardMapper.deleteOne(no); // 삭제

        boardVO = boardMapper.selectOne(no);
        log.info(boardVO);
    }

public interface BoardService {

    void register(BoardDTO boardDTO);

    List<BoardDTO> getAll();

    BoardDTO getOne(Integer no);

    void updateOne(BoardDTO boardDTO);

    void deleteOne(Integer no);

}
    @Override
    public void deleteOne(Integer no) {
        boardMapper.deleteOne(no);
    }
    @PostMapping("/remove")
    public String remove(Integer no, RedirectAttributes redirectAttributes) {
        log.info("-----remove----");
        log.info("no: " + no);

        boardService.deleteOne(no);
        return "redirect:/board/list";
    }

 

 


 

10.  비밀번호 확인

  • PasswdVO 작성
  • BoardMapper 인터페이스 작성
  • BoardMapperxml 작성
  • BoardMapperTest 클래스 작성
  • PasswdDTO 작성
  • BoardService 작성
  • BoardServiceImpl 작성
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PasswdVO {
    private int no;
    private String passwd;
}
public interface BoardMapper {

    ...

    BoardVO selectOneByPasswd(PasswdVO passwdVO);
}
    <select id="selectOneByPasswd" resultType="com.example.spring_ex_01_2404.domain.BoardVO">
        SELECT * FROM tbl_board WHERE no = #{no} AND passwd = SHA2(#{passwd}, 256)
    </select>
    @Test
    public void selectOneByPasswd() {
        PasswdVO passwdVO = PasswdVO.builder()
                .no(12)
                .passwd("1234").build();
        BoardVO boardVO = boardMapper.selectOneByPasswd(passwdVO);

        log.info(boardVO);
    }

@ToString
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PasswdDTO {
    private Integer no;
    private String passwd;
}
public interface BoardService {
    ...
    
    boolean isCurrentPasswd(PasswdDTO passwdDTO);
}
 @Override
    public boolean isCurrentPasswd(PasswdDTO passwdDTO) {
        PasswdVO passwdVO = modelMapper.map(passwdDTO, PasswdVO.class);

        return boardMapper.selectOneByPasswd(passwdVO) != null;
    }
    @Test
    public void isCurrentPasswd() {
        PasswdDTO passwdDTO = PasswdDTO.builder()
                .no(12)
                .passwd("1234").build();
        log.info(boardService.isCurrentPasswd(passwdDTO));
    }

 

JSP 코드 - 수정 or 삭제에 따라 버튼 내용 및 문구 타이틀 변경 추가
 <div class="card-body">
    <h3>${reason == "incorrect" ? "틀린 비밀번호 입니다." : "비밀번호를 입력해주세요!"}</h3>
    <form action="/board/passwd" method="post">
        <input type="hidden" name="no" value="${no}">
        <input type="hidden" name="mode" value="${mode}">
        <div class="input-group">
            <span class="input-group-text">Password</span>
            <input type="password" name="passwd" placeholder="비밀번호">
        </div><br>
        <div class="my-4">
            <div class="float-end">
                <button type="submit" class="btn btn-danger">${mode == "remove" ? "삭제" : "수정"}</button>
            </div>
        </div>
    </form>
</div>

 

Controller - 수정, 삭제 하기 전 비밀번호 확인 코드
    @GetMapping("/modify")
    public String modify(Integer no, Model model,
                       HttpServletRequest request,
                       RedirectAttributes redirectAttributes) {

        // 1. 세션에 저장된 비밀번호를 불러옴.
        HttpSession session = request.getSession();
        String passwd = (String) session.getAttribute("passwd");

        // 2. 비밀번호가 없으면 비밀번호 입력 페이지로 리다이렉트
        if (passwd == null || passwd.isEmpty()) {
            redirectAttributes.addAttribute("no", no);
            redirectAttributes.addAttribute("mode", "modify");
            return "redirect:/board/passwd";
        }

        // 3. 비밀번호가 틀리면 비밀번호 입력 페이지로 리다이렉트
        if (!boardService.isCurrentPasswd(PasswdDTO.builder().no(no).passwd(passwd).build())) {
            session.removeAttribute("passwd");
            redirectAttributes.addAttribute("no", no);
            redirectAttributes.addAttribute("mode", "modify");
            redirectAttributes.addFlashAttribute("reason", "incorrect");
            return "redirect:/board/passwd";
        }

        BoardDTO boardDTO = boardService.getOne(no);
        model.addAttribute("dto", boardDTO);
        
        return "/board/modify";
    }

    @RequestMapping("/remove")
    public String remove(Integer no, HttpServletRequest request, RedirectAttributes redirectAttributes) {

        // 1. 세션에 저장된 비밀번호를 불러옴.
        HttpSession session = request.getSession();
        String passwd = (String) session.getAttribute("passwd");

        // 2. 비밀번호가 없으면 비밀번호 입력 페이지로 리다이렉트
        if (passwd == null || passwd.isEmpty()) {
            redirectAttributes.addAttribute("no", no);
            redirectAttributes.addAttribute("mode", "remove");
            return "redirect:/board/passwd";
        }

        // 3. 비밀번호가 틀리면 비밀번호 입력 페이지로 리다이렉트
        if (!boardService.isCurrentPasswd(PasswdDTO.builder().no(no).passwd(passwd).build())) {
            session.removeAttribute("passwd");
            redirectAttributes.addAttribute("no", no);
            redirectAttributes.addAttribute("mode", "remove");
            redirectAttributes.addFlashAttribute("reason", "incorrect");
            
            return "redirect:/board/passwd";

        }

        // 4. 게시물 삭제
        log.info("-----remove----");
        boardService.deleteOne(no);

        // 5. 삭제 후 비밀번호 삭제
        session.removeAttribute("passwd");

        return "redirect:/board/list";
    }

    // 비밀번호 입력 페이지
    @GetMapping("/passwd")
    public void passwd(Integer no, String mode, Model model) {
        model.addAttribute("no", no);
        model.addAttribute("mode", mode);
    }

    @PostMapping("/passwd")
    public String passwdPost(Integer no, String mode, String passwd, HttpServletRequest request,
                             RedirectAttributes redirectAttributes) {
        log.info("...passwdPost()");
        log.info(mode);

        // 1. 전달받은 비밀번호를 세션에 저장
        HttpSession session = request.getSession();
        session.setAttribute("passwd", passwd);

        // 2. 해당 처리 페이지로 리다이렉트
        redirectAttributes.addAttribute("no", no);
        return "redirect:/board/" + mode;
    }

 

 

 

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

 


1.  activity_main.xml

데이터 피커, 에디트텍스트, 버튼을 1개씩 생성

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <DatePicker
        android:id="@+id/datePicker"
        android:datePickerMode="spinner"
        android:calendarViewShown="false"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:padding="10dp"
        android:background="#F0FFE6"
        android:layout_weight="1"
        android:lines="8"/>

    <Button
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:enabled="false"
        android:text="BUTTON" />

</LinearLayout>

 

android:lines="8"

 

    ✓  에디트텍스트를 8행으로 설정

 

android:enabled="false"


    ✓  초기에 버튼을 disable 되게 하고, java 코드에서 enable 되게 함

 


 

2.  Kotlin 코드 작성 및 수정

  • activity_main의 3개 위젯에 대응할 위젯 변수 3개를 선언
  • 파일 이름을 저장할 문자열 변수 1개를 선언. 파일 이름을 '연_월_일.txt'로 지정
  • 위젯 변수에 activity_main.xml의 위젯을 대입
  • 데이트피커를 설정
  • Calendar 클래스를 이용하여 현재 날짜의 년, 월, 일을 구한 후 데이트피커를 초기화 ➡️ 데이트피커의 날짜가 변경되면 변경된 날짜에 해당하는 일기 파일(연_월_일.txt)의 내용을 에디트텍스트로 보여줌
  • 현재 날짜의 파일(연_월_일.txt)을 읽어 일기의 내용을 반환하는 readDiary() 메서드 완성
class MainActivity : AppCompatActivity() {
    lateinit var datePicker: DatePicker
    lateinit var editText: EditText
    lateinit var btn: Button
    lateinit var fileName: String // 파일 이름 저장

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        title = "간단 일기장"

        datePicker = findViewById(R.id.datePicker)
        editText = findViewById(R.id.editText)
        btn = findViewById(R.id.btn)

        val calendar = Calendar.getInstance()
        val calendarYear = calendar.get(Calendar.YEAR)
        val calendarMonth = calendar.get(Calendar.MONTH)
        val calendarDay = calendar.get(Calendar.DAY_OF_MONTH)

        fileName = "${calendarYear}_${calendarMonth + 1}_${calendarDay}.txt"
        val str = readDiary(fileName) // 날짜에 해당하는 일기 파일을 읽기
        editText.setText(str) // 에디트텍스트에 일기 내용을 출력
        btn.isEnabled = true // 버튼 활성화


        datePicker.init(
            calendarYear,
            calendarMonth,
            calendarDay,
            DatePicker.OnDateChangedListener() { datePicker: DatePicker, year: Int, month: Int, day: Int ->
                fileName = "${year}_${month + 1}_${day}.txt"
                val string = readDiary(fileName) // 날짜에 해당하는 파일을 읽기
                editText.setText(string) // 에디트텍스트에 일기 내용을 출력
                Toast.makeText(applicationContext, fileName, Toast.LENGTH_SHORT).show()
                btn.isEnabled = true // 버튼 활성화
            })

        btn.setOnClickListener {
            val outputStream = openFileOutput(fileName, Context.MODE_PRIVATE)
            val string = editText.text.toString()
            outputStream.write(string.toByteArray())
            outputStream.close()
            Toast.makeText(applicationContext, "${fileName} 이 저장됨", Toast.LENGTH_SHORT).show()
        }
    }

    private fun readDiary(fileName: String): String? {
        var diaryStr: String? = null
        val inputStream: FileInputStream

        try {
            inputStream = openFileInput(fileName)
            val txt = ByteArray(inputStream.available())
            inputStream.read(txt)
            inputStream.close()
            diaryStr = txt.toString(Charsets.UTF_8).trim()
            btn.text = "수정하기"
        } catch (e: IOException) {
            editText.hint = "일기 없음"
            btn.text = "새로 저장"
        }
        return diaryStr;
    }

}

 

inputStream = openFileInput(fileName)


    ✓ 일기 파일을 열어 입력 파일 스트림에 저장. 파일이 없으면 예외가 발생해서 catch 구문이 실행

 


 

3.   프로젝트 실행 및 결과 확인

선택된 날짜에 쓴 일기가 있다면 일기 내용이 보이고 버튼이 <수정하기>로 바뀜
선택된 날짜에 일기가 없다면 에디트텍스트에 '일기 없음' 힌트가 보이고 버튼이 <새로 저장>으로 바뀜

 

 

 


4. 파일 확인

AVD가 켜진 상태에서 Divice File Explorer 실행
View  ▶️  Tool Windows  ▶️  Divice File Explorer

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  파일 처리의 응용

음악, 영상, 그림 파일 등은 여러 응용 프로그램에서 사용되는 경우가 많음.
  📍  예를 들어 MP3 파일을 안드로이드에 내장된 앱에서 들을 수도 있고 별도의 음악 플레이어로 들을 수도 있음
         ➡️  이런 경우 SD 카드에 저장하여 활용

안드로이드에서는 SD 카드에 저장된 데이터에 특별한 인증 절차 없이 접근 가능하며 제한된 공간의 내장 메모리보다 훨씬 큰 공간을 사용할 수 있으며 확장성도 뛰어남. 실제 안드로이드폰의 경우 주로 마이크로 SD 카드를 장착하여 사용. AVD에도 가상 SD 카드를 장착할수 있음.

 

AVD Manager를 실행 후 해당 AVD 이름의 오른쪽에 있는 Edit 아이콘을 클릭한후 Show Advanced Settings를 클릭하면 사용하는 AVD에 장착된 SD 카드를 확인할 수 있다. 만약 가상 SD 카드가 만들어져 있지 않으면 원하는 크기를 새로 입력.

 

 

Target SDK 버전 변경


    Android SDK 30 버전에서 SD 카드의 처리 방법이 대폭 변경되어 접근 거부가 발생할 수 있음
      ➡️  이럴 때는 Target SDK 버젼을 29로 변경

android {
    compileSdk 32

    defaultConfig {
        applicationId "kr.somin.sdkstream"
        minSdk 26
        targetSdk 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

 

1)  SD 카드에서 파일 읽기

💫  폴더 경로가 다른 것만 제외하면 SD 카드의 기본적인 경로는 내장 메모리에서 파일을 읽을 때와 별다른 차이가 없음
💫  먼저 Device File Explorer에서 /sdcard 폴더 또는 /storage/emulated/0 폴더에 적당한 텍스트 파일을 upload
💫  AVD에서 sd 카드의 경로는 절대적인 것이 아니며 SDK 버전에 따라 달라질수 있음

 

 

💫  AndroidManifest.xml 파일에 SD 카드를 사용할 수 있도록 퍼미션을 지정해야 함
       ➡️  AndroidManifest.xml에 퍼미션 및 application 관련 속성을 추가

  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application
        android:requestLegacyExternalStorage="true"

 

SD 카드에서 파일 읽기
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnRead"
        android:text="SD 카드에서 파일 읽기" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/editSD"
        android:lines="10" />

</LinearLayout>
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val btnRead = findViewById<Button>(R.id.btnRead)
        val editSD = findViewById<EditText>(R.id.editSD)
        
        ActivityCompat.requestPermissions(this, 
            arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
            Context.MODE_PRIVATE)
        
        btnRead.setOnClickListener {
            try {
                val inputStream = FileInputStream("/storage/emulated/0/Download/test.txt")
                val txt = ByteArray(inputStream.available())
                inputStream.read(txt)
                editSD.setText(txt.toString(Charsets.UTF_8))
                inputStream.close()
            } catch (e: IOException) {
                println(e.printStackTrace())
                Toast.makeText(applicationContext, "에러", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

 

ActivityCompat.requestPermissions(this,
    arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
    Context.MODE_PRIVATE)


    앱에서 파일 엑세스 작업을 허용할지를 묻는 창이 나옴. <허용>을 클릭.

val inputStream = FileInputStream("/storage/emulated/0/Download/test.txt")


    파일 입력 스트림을 생성할 때 SD 카드의 절대 경로를 FileInputStream() 생성자에 지정

 


2)  SD 카드에 폴더 및 파일 생성

SD 카드의 파일을 좀 더 정밀하게 처리하려면 우선 Environment 클래스의 정적 메소드를 이용하여 SD 카드의 동작 여부와 관련 폴더 경로를 구해야 함

SD 카드에 폴더 및 파일 생성
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnMkDir"
        android:text="SD 카드에 디렉토리 생성" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnRmDir"
        android:text="SD 카드에 디렉토리 삭제" />

</LinearLayout>
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.layout_01)
        
        ActivityCompat.requestPermissions(this, 
            arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
            Context.MODE_PRIVATE)

        val btnMkdir = findViewById<Button>(R.id.btnMkDir)
        val btnRmdir = findViewById<Button>(R.id.btnRmDir)

        val strSDpath = Environment.getExternalStorageDirectory().absolutePath
        val myDir = File("${strSDpath}/MyDir")

        btnMkdir.setOnClickListener {
            myDir.mkdir()
        }

        btnRmdir.setOnClickListener {
            myDir.delete()
        }
    }
}

 

val strSDpath = Environment.getExternalStorageDirectory().absolutePath

 

    ✓  SD 카드의 절대 경로를 돌려주는데, 대개 /sdcard 폴더 또는 /storage/emulated/0

    ✓  getExternalStorageDirectory() 이외에도 시스템 폴더를 반환하는 getRootDirectory(),

         데이터 폴더를 반환하는 getDataDirectory()등이 있음

 

val myDir = File("${strSDpath}/MyDir")


    ✓  SD 카드의 경로 아래에 MyDir 폴더를 생성하기 위한 File형 변수를 설정

 

 

 


3)  특정 폴더의 하위 폴더 및 파일 목록

지정한 폴더의 하위 폴더 및 파일 목록에 접근하려면 File.listFiles() 메서드를 사용  ➡️  반환형은 File[] 형

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnFileList"
        android:text="시스템 폴더의 폴더/파일 목록" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/editFileList" />

</LinearLayout>
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.layout_02)

        val btnFileList = findViewById<Button>(R.id.btnFileList)
        val editFileList = findViewById<EditText>(R.id.editFileList)
        btnFileList.setOnClickListener {
            val sysDir = Environment.getRootDirectory().absolutePath
            val sysFiles = File(sysDir).listFiles()

            var strFname: String
            for (i in sysFiles.indices) {
                if (sysFiles[i].isDirectory == true)
                    strFname = "<폴더> " + sysFiles[i].toString()
                else
                    strFname = "<파일> " + sysFiles[i].toString()

                editFileList.setText(editFileList.text.toString() + "\n" + strFname)
            }
        }
    }
}

 

val sysDir = Environment.getRootDirectory().absolutePath


    ✓  안드로이드 시스템 경로를 반환

 

val sysFiles = File(sysDir).listFiles()

 

    ✓  시스템 폴더의 폴더 및 파일 목록을 구해서 배열로 반환

 

if (sysFiles[i].isDirectory == true)


    ✓  현재 파일이 폴더인지 파일인지 확인

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  파일 처리의 기본

안드로이드에서 파일을 읽고 쓰는 방법에는 Java에서 제공하는 파일 관련 클래스를 사용하는 방법과 안드로이드에서 제공하는 파일 관련 클래스를 사용하는 방법이 있음. 아무 곳에서나 파일을 읽고 쓸 수는 없고 제한된 폴더나 SD 카드 등에서만 가능.


1)  내장 메모리 파일 처리

🐰  앱을 종료했다가 다음에 다시 실행할 때 사용하던 곳 부터 이어서 작업하고 싶은 경우,내장 메모리에 파일을 저장하고 읽어오는 방식을 활용
🐰  내장 메모리의 저장 위치는 /data/data/패키지명/files 폴더
      📍 일반적으로 앱마다 다른 패키지명을 사용하므로 앱별로 고유의 저장 공간이 있다고 생각하면 됨

🐰  파일을 읽기 위해 먼저 안드로이드 Context 클래스의 openFileInput() 메서드를 사용 ▶️ FileInputStream을 반환
🐰  파일을 쓰기 위해 openFileOutput() 메서드를 사용 ▶️ FileOutputStream을 반환
🐰  Java에서 제공하는 파일을 읽거나 쓰는 java.io.FileInputStream 클래스와 java.io.FileOuptputStream 클래스의 read(), write() 메서드를 사용하여 파일을 처리

 

내장 메모리에서 파일을 읽거나 쓰는 일반적인 절차
  • openFileInput() / openFileOutput() 으로 파일 열기 : FileInputStream / FileOutputStream 반환
  • read() / write()로 파일 읽기 / 쓰기
  • close()로 파일 닫기
xml 코드
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnWrite"
        android:text="내장 메모리에 파일 쓰기" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnRead"
        android:text="내장 메모리에 파일 읽기" />

</LinearLayout>

 

kotlin 코드
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val btnWrite = findViewById<Button>(R.id.btnWrite)
        val btnRead = findViewById<Button>(R.id.btnRead)

        btnWrite.setOnClickListener {
            val outputStream = openFileOutput("file.txt", Context.MODE_PRIVATE)
            val string = "파일 처리 테스트"
            outputStream.write(string.toByteArray())
            outputStream.close()
            Toast.makeText(applicationContext, "file.txt가 생성됨.", Toast.LENGTH_SHORT).show()
        }

        btnRead.setOnClickListener {
            try {
                val inputStream = openFileInput("file.txt")
                val txt = ByteArray(30)
                inputStream.read(txt)
                val string = txt.toString(Charsets.UTF_8)
                Toast.makeText(applicationContext, string, Toast.LENGTH_SHORT).show()
            } catch (e : Exception) {
                Toast.makeText(applicationContext, "파일 없음", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

 

val outputStream = openFileOutput("file.txt", Context.MODE_PRIVATE)


    ✓  file.txt로 파일을 쓰기 모드로 열기. 파일의 경로는 /data/data/패키지명/files/file.txt가 됨
    ✓  파일 모드에는 쓰기(write)를 위해 MODE_PRIVATE (생성한 앱에서만 사용 가능)나 MODE_APPEND (파일이 존재하는 경우 내용 추가) 등을 사용

  • MODE_PRIVATE : 혼자만 사용하는 모드
  • MODE_APPEND : 파일이 존재할 경우 기존내용에 추가하는 모드
  • MODE_WORLD_READABLE : 다른 응용 프로그램과 파일 읽을 수 있음
  • MODE_WORLD_WRITABLE : 다른 응용 프로그램이 파일 기록할 수 있음
val txt = ByteArray(30)
inputStream.read(txt)
val string = txt.toString(Charsets.UTF_8)

 

    ✓  byte[] 형 변수 txt에 입력 파일에서 읽어온 데이터를 저장. txt를 문자열로 변환.

 


 

2) raw 폴더 파일 처리


🐰  프로젝트의 /res/raw 폴더에 필요한 파일을 저장하기 위해 사용하는 방법
    📍 기본적으로 /res 아래에 raw 폴더를 생성하고 프로젝트에서 사용할 파일을 넣어둠


🐰  Java 코드에서 openRawResource() 메서드를 사용하여 접근할 수 있으며 FileInputStream 클래스 대신 InputStream 클래스를 사용
🐰  /res/raw는 프로젝트에 포함된 폴더이므로 읽기 전용으로만 사용 가능하고 쓰기는 할 수 없음. 쓰기에는 내장 메모리나 SD 카드를 사용해야 함.

 

프로젝트의 res 폴더에 raw 폴더를 생성하고, 임의의 내용을 입력한 *.txt파일을 복사
텍스트 파일은 UTF-8 형식으로 저장

 

xml 코드
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnRead"
        android:text="/res/raw에서 파일 읽기" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/editRaw"
        android:lines="10" />

</LinearLayout>

 

kotlin 코드
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.layout_01)

        val btnRead = findViewById<Button>(R.id.btnRead)
        val editRaw = findViewById<EditText>(R.id.editRaw)

        btnRead.setOnClickListener {
            val inputStream = resources.openRawResource(R.raw.test)
            val txt = ByteArray(inputStream.available())
            inputStream.read(txt)
            editRaw.setText(txt.toString(Charsets.UTF_8))
            inputStream.close()
        }
    }
}

 

val inputStream = resources.openRawResource(R.raw.test)

 

   ✓  resources는 현재 패키지의 리소스를 반환. openRawResource()- /res/raw 파일을 읽기용으로 열기.

 

val txt = ByteArray(inputStream.available())


   ✓  inputStream.available() : 입력 스트림에서 읽을 수 있는 바이트 수를 반환. 결국 txt 파일의 크기 만큼 지정.

 

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


 

💡  DTO : 컨트롤러에서 서비스로, 혹은 서비스에서 컨트롤러로 데이터를 주고 받을 때 사용
       VO : 서비스는 DAO와 데이터 주고 받을 때는 VO를 사용하기 때문에 서비스에서 DTO ↔ VO 작업이 필요
  • DTO : 컨트롤러 ↔ 서비스
  • VO : 서비스 ↔ DAO

1.  콘솔창에서 데이터베이스 작업 

CREATE TABLE `tbl_board` (
    `no` int auto_increment primary key,  
    `title` varchar(100) not null,     // 제목
    `content` text not null ,          // 내용
    `writer` varchar(50) not null ,    // 작성자
    `passwd` varchar(100) not null ,   // 비밀번호
    `addDate` datetime,                // 작성날짜
    `hit` int default 0                // 조회수
);

 


 

2.  BoardVO 작성 

테이블에 저장할 데이터를 담거나, 테이블에서 들고온 데이터를 담는 용도

주로 DAO (데이터베이스 처리하는 파트) 에서 사용

@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class BoardVO {

    private Integer no;
    private String title;      // 제목
    private String content;    // 내용
    private String writer;     // 작성자
    private String passwd;     // 비밀번호
    private LocalDate addDate; // 작성일
    private Integer hit;       // 조회수

}

 


3.  VO를 이용해서 데이터베이스에 저장하는 테스트 코드 작성

 작성 순서

 

  1)  BoardMapper 인터페이스 작성 - 객체 받아 데이터베이스에 저장, 수정, 삭제를 실행하는 메소드 작성

  2) BoardMapperxml 작성 - 데이터베이스 쿼리문 작성

  3)  BoardMapperTest 클래스 작성 - 실행 테스트

 

 

public interface BoardMapper {
    void insert(BoardVO boardVO); // DB 저장하는 메소드
}

 

<mapper namespace="com.example.spring_ex_01_2404.mapper.BoardMapper">
    <insert id="insert">
        INSERT INTO tbl_board (title, content, writer, passwd, addDate)
        VALUES (#{title}, #{content}, #{writer}, SHA2(#{passwd},256), now())
    </insert>
</mapper>

 

  ✓  mysql로 비밀번호 암호화 할 경우 SHA2(입력값, 256) 함수 사용

  ✓ addDate는 글 작성 시간이 들어가니 now() 함수를 사용

  ✓ hit는 작성시에는 0이니 컬럼의 기본값을 사용

@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
class BoardMapperTest {
    @Autowired(required = false)
    private BoardMapper boardMapper;

    @Test
    public void testInsert() {
        BoardVO boardVO = BoardVO.builder() // 빌더를 이용해서 BoardVO 객체를 생성
                .title("스프링 테스트")
                .content("스프링 테스트 중...")
                .writer("river")
                .passwd("1234")
                .addDate(now())
                .build();
        boardMapper.insert(boardVO);
    }
}
02:34:06 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.insert] ==> Preparing: INSERT INTO tbl_board (title, content, writer, passwd, addDate) VALUES (?, ?, ?, SHA2(?,256), now())
02:34:06 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.insert] ==> Parameters: 스프링 테스트(String), 스프링 테스트 중...(String), river(String), 1234(String)
02:34:06 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.insert] <== Updates: 1

 


4.  Service 작업

BoardDTO → BoardService → BoardServiceImpl → 테스트 코드
@ToString
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BoardDTO {
    private Integer no;
    private String title;
    private String content;
    private String writer;
    private String passwd;
    private LocalDate addDate;
    private Integer hit;
}

 

public interface BoardService {

    void register(BoardDTO boardDTO);
}

 

@Log4j2
@Service
@RequiredArgsConstructor // 생성자 객체 주입. private final로 선언된 참조변수에 객체를 저장하는 생성자 작성.
public class BoardServiceImpl implements BoardService{
    private final BoardMapper boardMapper;
    private final ModelMapper modelMapper;

    @Override
    public void register(BoardDTO boardDTO) {
        log.info(boardDTO);
        BoardVO boardVO = modelMapper.map(boardDTO, BoardVO.class);
        log.info(boardVO);
        boardMapper.insert(boardVO);
    }
}

 

@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
class BoardServiceImplTest {
    @Autowired
    private BoardService boardService;

    @Test
    public void testRegister() {
        BoardDTO boardDTO = BoardDTO.builder()
                .title("service test")
                .content("service test...")
                .writer("user")
                .passwd("12345")
                .addDate(LocalDate.now())
                .build();
        boardService.register(boardDTO);
    }
}
03:23:33 INFO [com.example.spring_ex_01_2404.service.BoardServiceImpl] BoardDTO(no=null, title=service test, content=service test..., writer=user, passwd=12345, addDate=2024-05-01, hit=null)
03:23:33 INFO [com.example.spring_ex_01_2404.service.BoardServiceImpl] BoardVO(no=null, title=service test, content=service test..., writer=user, passwd=12345, addDate=2024-05-01, hit=null)
03:23:33 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.insert] ==> Preparing: INSERT INTO tbl_board (title, content, writer, passwd, addDate) VALUES (?, ?, ?, SHA2(?,256), now())
03:23:33 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.insert] ==> Parameters: service test(String), service test...(String), user(String), 12345(String)
03:23:33 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.insert] <== Updates: 1

 


 

5.  등록페이지 작성

  • /board/add로 접근할 경우 등록 페이지 출력
  • 등록페이지에서 입력 후 submit을 하면 데이터베이스에 글이 등록
작업순서 
  • BoardController 작성
  • get으로 /board/add에 접근할 경우 실행할 메서드 작성
  • /board/add로 정상적으로 접근되는지 확인
  • webapp에 add.jsp 코딩
  • controller에 post로 /board/add에 접근할 경우 실행할 메서드 작성
  • 테스트 후 입력한 데이터가 제대로 전달되는지 확인
  • addPost()를 수정해서 서비스로 연결
  • 데이터베이스에 저장 확인
  • addPost()를 수정해서 /board/list 이동 확인
@Log4j2
@Controller
@RequestMapping("/board")
@RequiredArgsConstructor
public class BoardController {
    private final BoardService boardService;

    @GetMapping("/add")
    public void add() {
        log.info("add board...");
    }

    @PostMapping("/add")
    public String addPost(@Valid BoardDTO boardDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
        log.info("board addPost()...");

        if (bindingResult.hasErrors()) {
            log.info("has error...");
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            return "redirect:/board/add";
        }

        log.info(boardDTO);
        boardService.register(boardDTO);

        return "redirect:/board/list";

    }
 }

 

 <form  method="post">
    <div class="input mb-3">
        <span class="input-group-text">Title</span><br>
        <input type="text" name="title" placeholder="Title"><br><br>
    </div>
    <div class="input mb-3">
        <span class="input-group-text">Content</span><br>
        <textarea name="content" cols="60" rows="18"></textarea><br><br>
    </div>
    <div class="input mb-3">
        <span class="input-group-text">Writer</span><br>
        <input type="text" name="writer" placeholder="Writer"><br><br>
    </div>
    <div class="input mb-3">
        <span class="input-group-text">Password</span><br>
        <input type="password" name="passwd" placeholder="비밀번호"><br><br>
    </div>
    <div class="float-end">
        <button type="submit" name="submitBtn">Submit</button>
    </div>
</form>

 

 


 

6.  목록페이지

  • DAO에 목록을 불러오는 selectAll() 메서드 작성
  • 테스트 코드 작성 후 테스트
  • Service 작업
  • 테스트 코드 작성 후 테스트
  • 컨트롤러에 /board/list 경로와 매핑을 한 list() 작성
  • list.jsp 작성
public interface BoardMapper {

    void insert(BoardVO boardVO); // DB 저장하는 메소드

    List<BoardVO> selectAll(); // DB에 저장되어 있는 리스트 목록
}
    <select id="selectAll" resultType="com.example.spring_ex_01_2404.domain.BoardVO">
        SELECT * FROM tbl_board ORDER BY no DESC
    </select>
    @Test
    public void selectAll() {
        List<BoardVO> boardVOList = boardMapper.selectAll();
        for(BoardVO boardVO : boardVOList) {
            log.info(boardVO);
        }
    }
public interface BoardService {

    void register(BoardDTO boardDTO);

    List<BoardDTO> getAll();

}
    @Override
    public List<BoardDTO> getAll() {
        List<BoardVO> voList = boardMapper.selectAll(); // dao에서 데이터베이스에서 들고온 VO리스트를 리턴
        List<BoardDTO> dtoList = new ArrayList<>();
        for (BoardVO boardVO: voList) {
            // 개별 VO를 DTO로 변환.
            BoardDTO boardDTO = modelMapper.map(boardVO, BoardDTO.class);
            dtoList.add(boardDTO); // DTO리스트에 저장.
        }
        return dtoList;
    }
  @Test
    public void getAll() {
        List<BoardDTO> boardDTOList = boardService.getAll();
        for (BoardDTO boardDTO : boardDTOList) {
            log.info(boardDTO);
        }
    }
  @RequestMapping("/list")
    public void list(Model model) {
        log.info("todo list...");
        model.addAttribute("dtoList", boardService.getAll());
    }
<div class="card">
    <div class="card-header">Board List</div>

        <div class="card-body">
            <table class="table">
                <thead>
                    <tr>
                        <th scope="col">No</th>
                        <th scope="col">Title</th>
                        <th scope="col">Writer</th>
                        <th scope="col">Date</th>
                    </tr>
                </thead>

                <tbody>
                <c:forEach var="dto" items="${dtoList}">
                    <tr>
                         <th scope="row">${dto.no}</th>
                         <td>${dto.title}</td>
                         <td>${dto.writer}</td>
                         <td>${dto.addDate}</td>
                    </tr>
                </c:forEach>
                </tbody>
            </table>
        </div>
    </div>
</div>

 


7.  조회

리스트에서 클릭을 하면 해당 글을 보여주는 페이지 구현

  • DAO에 게시물을 불러오는 selectOne() 메서드 작성
  • 테스트 코드 작성  후 테스트
  • Service 작업
  • 컨트롤러에 /board/read 경로와 매핑을 한 read() 작성
  • read.jsp 작성
  • updatehit 추가 (조회수 증가)
public interface BoardMapper {

    void insert(BoardVO boardVO); // DB 저장하는 메소드

    List<BoardVO> selectAll();

    BoardVO selectOne(Integer no); // 조회
    
    void updateHit(Integer no); // 조회수 증가
 
}
    <select id="selectOne" resultType="com.example.spring_ex_01_2404.domain.BoardVO">
        SELECT * FROM tbl_board WHERE no = #{no}
    </select>
    
    <update id="updateHit">
        UPDATE tbl_board SET hit = hit + 1 where no = #{no}
    </update>
    @Test
    public void selectOne() {
        BoardVO boardVO = boardMapper.selectOne(1);
        log.info(boardVO);
    }
11:46:50 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.selectOne] ==> Preparing: SELECT * FROM tbl_board WHERE no = ?
11:46:50 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.selectOne] ==> Parameters: 1(Integer)
11:46:50 TRACE [com.example.spring_ex_01_2404.mapper.BoardMapper.selectOne] <== Columns: no, title, content, writer, passwd, addDate, hit
11:46:50 TRACE [com.example.spring_ex_01_2404.mapper.BoardMapper.selectOne] <== Row: 1, 스프링 테스트, <<BLOB>>, river, 1234, 2024-05-01 00:00:00, 1
11:46:50 DEBUG [com.example.spring_ex_01_2404.mapper.BoardMapper.selectOne] <== Total: 1

 

public interface BoardService {

    void register(BoardDTO boardDTO);

    List<BoardDTO> getAll();

    BoardDTO getOne(Integer no);
}
    @Override
    public BoardDTO getOne(Integer no) {
        boardMapper.updateHit(no);
        BoardVO boardVO = boardMapper.selectOne(no);
        BoardDTO boardDTO = modelMapper.map(boardVO, BoardDTO.class);
        return boardDTO;
    }

 

 @GetMapping("/read")
    public void read(Integer no, Model model) {
        BoardDTO boardDTO = boardService.getOne(no);
        log.info(boardDTO);
        model.addAttribute("dto", boardDTO);
    }

 

<div class="card-body">
    <div class="input-group">
        <span class="input-group-text">No</span>
        <input type="text" name="tno" class="form-control" value="${dto.no}" readonly>
    </div> <br>
    <div class="input-group">
        <span class="input-group-text">Title</span>
        <input type="text" name="title" class="form-control" value="${dto.title}" readonly>
    </div><br>
    <div class="input-group">
       <span class="input-group-text">Content</span>
       <textarea name="title" class="form-control" readonly>${dto.content}</textarea>
    </div><br>
    <div class="input-group">
        <span class="input-group-text">Writer</span>
        <input type="text" name="writer" class="form-control" value="${dto.writer}" readonly>
    </div><br>
    <div class="input-group">
        <span class="input-group-text">Date</span>
        <input type="date" name="dueDate" class="form-control" value="${dto.addDate}" readonly>
    </div><br>
    <div class="input-group">
        <span class="input-group-text">Hit</span>
        <input type="text" name="writer" class="form-control" value="${dto.hit}" readonly>
    </div><br>
    <div class="my-4">
        <div class="float-end">
            <button type="submit" class="btn btn-primary">Modify</button>
            <button type="reset" class="btn btn-secondary">List</button>
        </div>
    </div>

    <script>
        document.querySelector('.btn-primary').addEventListener('click', function (e) {
            self.location = `/board/modify?no=${dto.no}&${boardDTO.link}`;
        }, false);
        document.querySelector('.btn-secondary').addEventListener('click', function (e) {
            self.location = "/board/list?${boardDTO.link}";
        }, false);
    </script>
</div>

 

 

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  검색 조건을 위한 화면 처리

검색 기능은 /WEB-INF/view/todo/list.jsp에서 이루어지므로 list.jsp에 검색 관련된 화면을 작성하기 위해서 <div class="card">를 하나 추가하고 검색에 필요한 내용들을 담을 수 있도록 구성.

 <div class="row content">
    <div class="col">
        <div class="card">
            <div class="card-body">
                <h5 class="card-title">Search </h5>
                <form action="/todo/list" method="get">
                    <input type="hidden" name="size" value="${pageRequestDTO.size}">
                    <div class="mb-3">
                        <input type="checkbox" name="finished">완료여부
                    </div>
                    <div class="mb-3">
                        <input type="checkbox" name="types" value="t">제목
                        <input type="checkbox" name="types" value="w">작성자
                        <input type="text"  name="keyword" class="form-control">
                    </div>
                    <div class="input-group mb-3 dueDateDiv">
                        <input type="date" name="from" class="form-control">
                        <input type="date" name="to" class="form-control">
                    </div>
                    <div class="input-group mb-3">
                        <div class="float-end">
                             <button class="btn btn-primary" type="submit">Search</button>
                             <button class="btn btn-info clearBtn" type="reset">Clear</button>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>

 

 

  ✓  화면에는 <form> 태그를 이용해서 검색 조건을 전송할 수 있도록 구성
  ✓  검색을 하는 경우 무조건 페이지는 1페이지가 되므로 별도의 파라미터를 지정하지 않음
 

TodoController에서는 log.info()를 이용해서 파라미터가 정상적으로 수집되는지 확인

 

  ✓  브라우저의 주소창에는 모든 검색 조건이 GET 방식의 쿼리 스트링으로 만들어짐


 

1) 화면에 검색 조건 표시하기


검색이 처리되기는 하지만 PageRequestDTO의 정보를 EL로 처리하지 않았기 때문에 검색 후에는 검색 부분이 초기화되는 문제가 있음
  ➡️  다른 페이지로 이동을 하면 검색값이 유지가 안됨
작성된 <div>에 EL을 적용할 때 가장 문제가 되는 부분은 제목 title, 작성자 writer를 배열로 처리하고 있으므로 화면에서 처리 할 때 좀 더 편하게 사용하기 위하여 PageRequestDTO에 별도의 메서드를 구성하도록 함

 public boolean checkType(String type) {
        if(this.types == null || this.types.length == 0) {
            return false;
        }
        return Arrays.asList(this.types).contains(type);
    }

 

 

list.jsp 코드 수정
 <form action="/todo/list" method="get">
    <input type="hidden" name="size" value="${pageRequestDTO.size}">
        <div class="mb-3">
            <input type="checkbox" name="finished"${pageRequestDTO.finished ? " checked" : ""}>완료여부
        </div>
        <div class="mb-3">
            <input type="checkbox" name="types" value="t"${pageRequestDTO.checkType("t") ? " checked" : ""}>제목
            <input type="checkbox" name="types" value="w"${pageRequestDTO.checkType("w") ? " checked" : ""}>작성자
            <input type="text"  name="keyword" class="form-control" value="${pageRequestDTO.keyword}">
        </div>
        <div class="input-group mb-3 dueDateDiv">
            <input type="date" name="from" class="form-control" value="${pageRequestDTO.from}">
            <input type="date" name="to" class="form-control" value="${pageRequestDTO.to}">
        </div>
        <div class="input-group mb-3">
            <div class="float-end">
                <button class="btn btn-primary" type="submit">Search</button>
                <button class="btn btn-info clearBtn" type="reset">Clear</button>
            </div>
        </div>    
</form>


 

2) 검색 조건 초기화 시키기


검색 영역에 Clear 버튼을 누르면 모든 검색 조건을 무효화 시키도록 '/todo/list'를 호출하도록 수정

 

화면의 버튼에 'clearBtn'이라는 class 속성을 추가
 <div class="input-group mb-3">
    <div class="float-end">
        <button class="btn btn-primary" type="submit">Search</button>
        <button class="btn btn-info clearBtn" type="reset">Clear</button>
    </div>
</div>
<script>
document.querySelector('.clearBtn').addEventListener('click', function (e) {
    self.location = '/todo/list';
});
</script>

 


 

3) 조회를 위한 링크 처리


검색 기능이 추가되면 문제 되는 것은 조회나 수정 화면에 있는 'List' 버튼. 기존과 달리 검색 조건들을 그대로 유지해야 하므로 상당히 복잡한 처리가 필요. 하지만 PageRequestDTO의 getLink()를 이용하면 상대적으로 간단히 처리 가능

 

getLink()를 통해서 생성되는 링크에서 검색 조건등을 반영해 주도록 수정
public String getLink() {
            StringBuilder builder = new StringBuilder();
            builder.append("page=" + this.page);
            builder.append("&size=" + this.size);
            if(this.finished){
                builder.append("&finished=on");
            }

            if(this.types != null && this.types.length > 0){
                for (int i = 0; i < this.types.length; i++) {
                    builder.append("&types=" + types[i]);
                }
            }

            if(this.keyword != null){
                try {
                    builder.append("&keyword=" + URLEncoder.encode(keyword, "UTF-8"));
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            }

            if (this.from != null) {
                builder.append("&from=" + from.toString());
            }

            if (this.to != null) {
                builder.append("&to=" + to.toString());
            }

            return builder.toString();
    }

 

getLink()는 모든 검색 / 필터링 조건을 쿼리 스트링으로 구성해야 한다. 그렇지 않으면 화면에서 모든 링크를 수정해야 하기 때문에 더 복잡하게 되기 때문. 주의할 부분은 한글이 가능한 keyword 부분은 URLEncoder를 이용해서 링크로 처리할 수 있도록 처리해야 됨

 

getLink()가 수정되면 화면에서 링크가 반영되는것 확인
목록 -> 상세 -> 수정 페이지 이동시에 쿼리 스트링이 반영되는 것 확인


 

4)  페이지 이동 링크 처리


페이지 이동에도 검색 / 필터링 조건은 필요하므로 자바스크립트로 동작하는 부분을 수정해야 함
기존에는 자바 스크립트에서 직접 쿼리 스트링을 추가해서 구성했지만, 검색 / 필터링 부분에 name이 page인 부분만 추가해서 <form> 태그를 submit으로 처리해 주면 검색 / 필터링 조건을 유지하면서도 페이지 번호만 변경하는 것이 가능

  document.querySelector(`.pagination`).addEventListener('click', function (e) {
                                    e.preventDefault();
                                    e.stopPropagation(); //이벤트가 상위 엘리먼트에 전달되지 않게 막아 준다

                                    const target = e.target;
                                    if(target.tagName !== 'A') {
                                        return;
                                    }
                                    const num = target.getAttribute('data-num');
                                    const frmPage = document.querySelector('form');
                                    frmPage.innerHTML += `<input type="hidden" name="page" value="\${num}">`;
                                    frmPage.submit();

                                    //self.location = `/todo/list?page=\${num}`;
                                });

 

검색/ 필터링을 유지하면서 페이지 이동하는 것을 확인

 


 

5) 수정 화면에서의 링크 처리


수정 화면인 modify.jsp에는 Remove, Modify, List 버튼이 존재하고 각 버튼에 대한 클릭 이벤트 수정

 

A. List 버튼 처리


PageRequestDTO의 getLink()를 이용해서 처리

document.querySelector('.btn-secondary').addEventListener('click', function (e) {
    self.location = `/todo/list?${pageRequestDTO.link}`;
});

 

B. Remove 버튼 처리


삭제된 후에 1페이지로 이동해도 문제가 되지 않지만 삭제 후에도 기존 페이지와 검색 / 필터링 정보를 유지하고 싶다면 PageRequestDTO를 이용할 수 있음

const frmModify = document.querySelector('form');
document.querySelector('.btn-danger').addEventListener('click', function () {
    frmModify.action = '/todo/remove';
    frmModify.method = 'post';
    frmModify.submit();
});

 

TodoController에서는 remove() 메서드가 이미 PageRequestDTO를 파라미터로 받고 있기 때문에 삭제 처리를 하고나서 리다이렉트하는 경로에 getLink()의 결과를 반영하도록 수정

  @PostMapping("/remove")
    public String remove(Long tno, PageRequestDTO pageRequestDTO , RedirectAttributes redirectAttributes) {
        log.info("-----remove----");
        log.info("tno: " + tno);

        todoService.remove(tno);
        redirectAttributes.addAttribute("page", 1);
        redirectAttributes.addAttribute("size", pageRequestDTO.getSize());
        return "redirect:/todo/list" + pageRequestDTO.getLink();
    }

 

 

C. Modify 버튼 처리


기존과 다른 처리가 필요한데, 검색 / 필터링 기능이 추가되면 Todo의 내용이 수정되면서 검색 / 필터링 조건에 맞지 않게 될 수 있기 때문.
  ➡️  예를 들어 검색 / 필터링에 날짜로 검색했는데 날짜를 수정하면서 검색 / 필터링 조건에 맞지 않아서 목록에 나오지 않을 수 있음

 

따라서 안전하게 하려면 검색 / 필터링의 경우 수정한 후에 조회 페이지로 이동하게 하고, 검색 / 필터링 조건은 없애는 것이 안전
검색 / 필터링 조건을 유지하지 않는다면 modify.jsp 에 선언된 <input type="hidden"> 태그의 내용은 필요하지 않으므로 삭제

 <form action="/todo/modify" method="post">
    <%-- <input type="hidden" name="page" value="${pageRequestDTO.page}">
         <input type="hidden" name="size" value="${pageRequestDTO.size}"> --%>
         <div class="input-group mb-3">
             <span class="input-group-text">Tno</span>
             <input type="text" name="tno" class="form-control" value="${dto.tno}" readonly>
         </div>

 

TodoController에서는 '/todo/list'가 아닌 '/todo/read'로 이동하도록 수정
  @PostMapping("/modify")
    public String midify(PageRequestDTO pageRequestDTO,
                         @Valid TodoDTO todoDTO,
                         BindingResult bindingResult,
                         RedirectAttributes redirectAttributes) {

        // 유효성 검사를 통과하지 못했더라도 값을 전송
        //redirectAttributes.addAttribute("page", pageRequestDTO.getPage());
        //redirectAttributes.addAttribute("size", pageRequestDTO.getSize());

        if (bindingResult.hasErrors()) {
            log.info("has error");
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            redirectAttributes.addAttribute("tno", todoDTO.getTno());
            return "redirect:/todo/modify";
        }
        redirectAttributes.addAttribute("tno", todoDTO.getTno());
        return "redirect:/todo/read";
    }

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts