안드로이드에서 파일을 읽고 쓰는 방법에는 Java에서 제공하는 파일 관련 클래스를 사용하는 방법과 안드로이드에서 제공하는 파일 관련 클래스를 사용하는 방법이 있음. 아무 곳에서나 파일을 읽고 쓸 수는 없고 제한된 폴더나 SD 카드 등에서만 가능.
1) 내장 메모리 파일 처리
🐰 앱을 종료했다가 다음에 다시 실행할 때 사용하던 곳 부터 이어서 작업하고 싶은 경우,내장 메모리에 파일을 저장하고 읽어오는 방식을 활용 🐰 내장 메모리의 저장 위치는 /data/data/패키지명/files 폴더 📍 일반적으로 앱마다 다른 패키지명을 사용하므로 앱별로 고유의 저장 공간이 있다고 생각하면 됨
🐰 파일을 읽기 위해 먼저 안드로이드 Context 클래스의 openFileInput() 메서드를 사용 ▶️ FileInputStream을 반환 🐰 파일을 쓰기 위해openFileOutput() 메서드를 사용 ▶️ FileOutputStream을 반환 🐰 Java에서 제공하는 파일을 읽거나 쓰는 java.io.FileInputStream 클래스와 java.io.FileOuptputStream 클래스의 read(), write() 메서드를 사용하여 파일을 처리
내장 메모리에서 파일을 읽거나 쓰는 일반적인 절차
openFileInput() / openFileOutput() 으로 파일 열기 : FileInputStream / FileOutputStream 반환
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 형식으로 저장
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>
✓ 화면에는 <form> 태그를 이용해서 검색 조건을 전송할 수 있도록 구성 ✓ 검색을 하는 경우 무조건 페이지는 1페이지가 되므로 별도의 파라미터를 지정하지 않음
TodoController에서는 log.info()를 이용해서 파라미터가 정상적으로 수집되는지 확인
✓ 브라우저의 주소창에는 모든 검색 조건이 GET 방식의 쿼리 스트링으로 만들어짐
1) 화면에 검색 조건 표시하기
검색이 처리되기는 하지만 PageRequestDTO의 정보를 EL로 처리하지 않았기 때문에 검색 후에는 검색 부분이 초기화되는 문제가 있음 ➡️ 다른 페이지로 이동을 하면 검색값이 유지가 안됨 작성된 <div>에 EL을 적용할 때 가장 문제가 되는 부분은 제목 title, 작성자 writer를 배열로 처리하고 있으므로 화면에서 처리 할 때 좀 더 편하게 사용하기 위하여 PageRequestDTO에 별도의 메서드를 구성하도록 함
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를 이용할 수 있음
기존과 다른 처리가 필요한데, 검색 / 필터링 기능이 추가되면 Todo의 내용이 수정되면서 검색 / 필터링 조건에 맞지 않게 될 수 있기 때문. ➡️ 예를 들어 검색 / 필터링에 날짜로 검색했는데 날짜를 수정하면서 검색 / 필터링 조건에 맞지 않아서 목록에 나오지 않을 수 있음
따라서 안전하게 하려면 검색 / 필터링의 경우 수정한 후에 조회 페이지로 이동하게 하고, 검색 / 필터링 조건은 없애는 것이 안전 검색 / 필터링 조건을 유지하지 않는다면 modify.jsp 에 선언된 <input type="hidden"> 태그의 내용은 필요하지 않으므로 삭제
날짜와 시간과 관련된 위젯으로는 타임피커, 데이트피커, 캘린더뷰, 크로노미터, 아날로그시계, 디지털시계 등이 있음
1) 아날로그시계와 디지털시계
🤓 아날로그 시계 AnalogClock와 디지털시계 DigitalClock는 화면에 시간을 표시하는 위젯으로 시계를 표현하는 용도로 쓰임. 이 둘은 View 클래스에서 상속받기 때문에 background 속성들을 설정할 수 있음. 디지털 시계는 textColor 같은 속성도 설정할 수 있음.
타임피커 TimePicker는 시간을, 데이트피커 DatePicker와 캘린더뷰 CalendarView는 날짜를 표시하고 조절하는 기능을 함 ⚡️ 캘린더뷰는 XML 속성이 여러가지인데, 그중에서 디폴트가 true인 showWeekNumber 속성은 현재 몇 주 차인지를 각주 의 맨 앞에 출력. 하지만 showWeekNumber 속성은 false로 하는 것이 더 깔끔하고 보기 좋음
롤리팝 API21 이후부터 타임피커와 데이트피커의 모양이 대폭 변경
📍 이전 모양의 버전을 사용하려면 타임피커는 android:timePickerMode="spinner" 속성을, 데이트피커는 android:datePickerMode="spinner" 속성을 추가
자동완성텍스트뷰 AutoCompleteTextView와 멀티자동완성텍스트뷰 MultiAutoCompleteTextView는 텍스트뷰 보다는 에디트텍스트 속성이 더 강함. 사용자가 단어의 일부만 입력해도 자동 완성되는데, 자동완성텍스트뷰는 단어 1개가 자동 완성되고, 멀티자동완성텍스트뷰는 쉼표(,)로 구분하여 여러 개의 단어가 자동 완성.
✓ 자동 완성 단어는 주로 Java 코드에서 배열로 설정하며 setAdapter() 메서드를 사용 ✓ completionHint : XML 속성 줌 목록에 나타날 힌트
✓ completionThreshold : 자동 완성 시작되기 전에 사용자가입력해야 하는 최소 문자 수
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_04)
// 자동완성텍스트뷰의 Java코드
val items = arrayOf("CSI-뉴욕", "CSI-라스베가스", "CSI-마이애미", "Friend", "Fringe", "Lost")
val auto = findViewById<AutoCompleteTextView>(R.id.autoCompleteTextView1)
val adapter = ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line, items)
auto.setAdapter(adapter)
val multi = findViewById<MultiAutoCompleteTextView>(R.id.multiAutoCompleteTextView1)
val tokenizer = MultiAutoCompleteTextView.CommaTokenizer()
multi.setTokenizer(tokenizer)
multi.setAdapter(adapter)
}
}
val items = arrayOf("CSI-뉴욕", "CSI-라스베가스", "CSI-마이애미", "Friend", "Fringe", "Lost")
> 자동 완성될 문자열을 배열로 정의
val adapter = ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line, items)
> ArrayAdapter는 뷰와 데이터를 연결
> 자동완성텍스트뷰와 items를 연결하는 역할을 하여 자동완성텍스트뷰에 items 배열의 내용이 출력 > 생성자의 두 번째 파라미터는 목록이 출력될 모양을 결정. simple_dropdown_item_1line 외에도 다양한 모양을 선택할 수 있음
val tokenizer = MultiAutoCompleteTextView.CommaTokenizer() multi.setTokenizer(tokenizer) multi.setAdapter(adapter)
> 쉼표로 구분하기 위한 객체를 생성하고 멀티자동완성텍스트 뷰에 설정
2) 프로그레스바, 시크바, 레이팅바
프로그레스바, 시크바, 레이팅바는 진행 상태를 표시하는 기능
프로그레스바 ProgressBar
🚀 작업의 진행 상태를 바 Bar 또는 원 형태로 제공 🚀 바 형태는 어느 정도 진행되었는지를 확인할 수 있지만, 원 형태는 현재 진행 중이라는 상태만 보여줌 🚀 주로 사용되는 XML 속성에는 범위를 지정하는 max, 시작 지점을 지정하는 progress, 두 번째 프로그레스바를 지정하는 secondaryProgress 등이 있음
시크바 SeekBar
🚀 프로그레스바의 하위 클래스로, 프로그레스바와 대부분 비슷하며 사용자가 터치로 임의 조절이 가능 🚀 음량을 조정하거나 동영상 재생 시 사용자가 재생 위치를 지정하는 용도로 사용할 수 있음
레이팅바 RatingBar
🚀 진행 상태를 별 모양으로 표시. 프로그레스바의 하위 클래스이므로 사용법이 비슷하며 서적, 음악, 영화 등에 대한 선호도를 나타낼 때 주로 사용 🚀 별의 개수를 정하는 numStars, 초깃값을 지정하는 rating, 한 번에 채워지는 개수를 정하는 stepSize 속성이 주로 사용
📍 대부분의 서비스에서는 검색 기능을 제공. 단순히 제목이나 내용 등을 검색하는 경우도 있고, 복잡한 검색 조건을 필터링 filtering 하는 경우도 있음 📍 검색 search는 'A 혹은 B 혹은 C'와 같이 찾고자 하는 경우 검색 조건들은 주로 OR 조건으로 연결되는 경우가 많음 ➡️ 예를 들어 '제목 or 내용 or 작성자'가 xxx인 경우와 같은 데이터가 있을수도 있고, 없을 수도 있음 📍 필터링 filtering은 'A인 동시에 B에도 해당'한다는 개념. 필터링은 주로 특정한 범위나 범주의 값으로 걸러내는 방식 ➡️ 예를 들어 '완료된 일 중에서 특정 날짜까지 끝난 Todo'는 'A & B'와 같이 AND라는 개념의 필터링이 적용
다음과 같은 검색과 필터링 조건을 구성하고 처리
제목 title과 작성자 writer 는 키워드 keyword를 이용하는 검색 처리
완료 여부를 필터링 처리
특정한 기간을 지정 from, to 한 필터링 처리
검색과 필터링에 필요한 데이터는 다음과 같이 구분 * 제목, 작성자 검색에 사용하는 문자열 - keyword * 완료 여부에 사용되는 boolean 타입 - finished * 특정 기간 검색을 위한 LocalDate 변수 2개 - from, to
2. 검색 / 필터링 조건의 결정
검색 기능을 개발할 때는 우선 검색 기능의 경우의 수를 구분하는 작업이 필요
검색 / 필터링의 종류가 '완료 여부, 제목, 작성자, 기간'들의 조합으로 구성
검색 종류를 types라고 지정해서 '제목(t), 작성자(w)'로 구분해서 검색의 실제 값은 검색 종류에 따라 키워드 keyword를 이용
검색은 목록 기능에 사용하는 PageRequestDTO에 필요한 변수들을 추가해서 구성
PageRequestDTO에 types, keyword, finished, from, to 변수를 새롭게 추가
private String[] types; // 검색 경우의 수 1) title 2) writer 3) title, writer
private String keyword; // 검색어
private boolean finished;
private LocalDate from;
private LocalDate to;
3. types에 따른 동적 쿼리
Mybatis에는 실행 시에 쿼리를 만들 수 있는 여러 태그들을 제공
1) foreach, if
🤓 <foreach>는 반복 처리를 위해 제공 🤓 <foreach>의 대상은 배열이나 List, Map, Set과 같은 컬렉션 계열이나 배열을 이용
실습을 위해 {"t", "w"}와 같은 types를 PageRequestDTO에 설정하고 테스트를 진행 기존의 TodoMapperTests 클래스에 새로운 메서드를 추가
📍 TodoMapper의 selectList()는 PageRequestDTO를 파라미터로 받고 있기 때문에 변경 없이 바로 사용 가능하므로 TodoMapper.xml만 수정하면 됨
TodoMapper.xml에서는 <select id="selectList"> 태그에 MyBatis의 <foreach>를 적용
<select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
select * from tbl_todo
<foreach collection="types" item="type">
#{type}
</foreach>
order by tno desc limit #{skip}, #{size}
</select>
👾 현재 PageRequestDTO의 types는 {"t", "w"}이므로 테스트 코드를 실행하면 다음과 같은 코드가 만들어지는 것을 확인 (쿼리문이 아직 완성된 상태가 아니라서 에러가 발생)
👾 sql 부분만 보면 다음과 같이 sql 이 실행 select * from tbl_todo ? ? order by tno desc limit ?, ? ✓ 't'와 'w'가 전달되었기 때문에 'from tbl_todo' 뒤에 두 개의 "?"가 생성된 것을 확인.
<if> 적용
<select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
select * from tbl_todo
<foreach collection="types" item="type" open="(" close=")" separator=" OR">
<if test="type == 't'.toString()">
title like concat('%', #{keyword}, '%')
</if>
<if test="type =='w'.toString()">
writer like concat('%', #{keyword}, '%')
</if>
</foreach>
order by tno desc limit #{skip}, #{size}
</select>
👾 open과 close를 이용해서 '()'와 배열을 처리하면서 중간에는 OR을 추가해서 다음과 같은 쿼리가 생성 select * from tbl_todo (title like concat('%', ?, '%') OR
writer like concat('%', ?, '%'))
order by tno desc limit ?, ?
2) <where>
📍 쿼리에 where 키워드가 빠져있음. 이것은 만일에 types가 없는 경우에는 쿼리문에 where를 생성하지 않기 위함. 📍 <where>는 태그 안쪽에서 문자열이 생성되어야만 where 키워드를 추가. 📍 이를 이용해서 types가 null이 아닌 경우를 같이 적용하면 다음과 같이 작성.
<select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
select * from tbl_todo
<where>
<if test="types != null and types.length > 0">
<foreach collection="types" item="type" open="(" close=")" separator=" OR">
<if test="type == 't'.toString()">
title like concat('%', #{keyword}, '%')
</if>
<if test="type =='w'.toString()">
writer like concat('%', #{keyword}, '%')
</if>
</foreach>
</if>
</where>
order by tno desc limit #{skip}, #{size}
</select>
3) <trim>과 완료 여부 / 만료일 필터링
완료 여부 (f)와 만료 기간 (d)에 대한 처리 📍 완료 여부는 PageRequestDTO의 finished 변수 값이 true인 경우에만 'finished = 1'과 같은 문자열이 만들어지도록 구성 ✓ 주의점은 앞의 검색 조건(제목, 작성자)이 있는 경우에는 'and finished = 1'의 형태로 만들어져야 하고, 그렇지 않은 경우에는 바로 'finished = 1'이 되어야 함 📍 MyBatis의 <trim>이 이런 경우에 사용.<where>과 유사하게 동작하면서 필요한 문자열을 생성하거나 제거할 수 있음
<if test = "finished">
<trim prefix="and">
finished = 1
</trim>
</if>
</where>
order by tno desc limit #{skip}, #{size}
</select>
👾 finished 값을 이용해서 SQL 문을 생성해 내는데 <trim>을 적용. prefix를 적용하게 되면 상황에 따라서 'and'가 추가.
<if test="from != null and to != null">
<trim prefix="and">
dueDate between #{from} and #{to}
</trim>
</if>
4) <sql> 과 <include>
MyBatis의 동적 쿼리 적용은 단순히 목록 데이터를 가져오는 부분과 전체 개수를 가져오는 부분에도 적용되어야 함 📍전체 개수를 가져오는 TodoMapper의 getCount()에 파라미터에 PageRequestDTO 타입을 지정한 이유 역시 동적 쿼리를 적용하기 위함 📍 MyBatis에는 <sql> 태그를 이용해서 동일한 SQL 조각을 재사용할 수 있는 방법을 제공 ➡️ 동적 쿼리 부분을 <sql>로 분리하고 동일하게 동적 쿼리가 적용될 부분은 <include>를 이용해서 작성
TodoMapper.xml에서 <sql id='search'>로 동적 쿼리 부분을 분리
<sql id="search">
<where>
<if test="types != null and types.length > 0">
<foreach collection="types" item="type" open="(" close=")" separator=" OR">
<if test="type == 't'.toString()">
title like concat('%', #{keyword}, '%')
</if>
<if test="type =='w'.toString()">
writer like concat('%', #{keyword}, '%')
</if>
</foreach>
</if>
<if test = "finished">
<trim prefix="and">
finished = 1
</trim>
</if>
<if test="from != null and to != null">
<trim prefix="and">
dueDate between #{from} and #{to}
</trim>
</if>
</where>
</sql>
<select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
select * from tbl_todo
<include refid="search"></include>
order by tno desc limit #{skip}, #{size}
</select>
<select id="getCount" resultType="int">
select COUNT(tno) FROM tbl_todo
<include refid="search"></include>
</select>
테스트 코드에서는 TodoMapper의 selectList()와 getCount()를 호출해서 결과를 확인
🚀 TodoMapper에서 TodoVO의 목록과 전체 데이터의 수를 가져온다면 이를 서비스 계층에서 한 번에 담아서 처리하도록 DTO를 구성하는 것이 좋음 🚀 작성하려는 DTO는 PageResponseDTO라는 이름을 생성하고 다음과 같은 데이터와 가능을 가지도록 구성
TodoDTO의 목록
전체 데이터의 수
페이지 번호 처리를 위한 데이터들 (시작 페이지 번호 / 끝 페이지 번호)
🚀 화면상에서 페이지 번호들을 출력하려면 현재 페이지 번호 page 와 페이지당 데이터의 수 size를 이용해서 계산할 필요가 있음 ➡️ 이 때문에 작성하려는 PageResponseDTO는 생성자를 통해서 필요한 page나 size 등을 전달받도록 구성
PageResponseDTO가 가져야 하는 데이터를 정리해서 클래스를 구성
public class PageResponseDTO<E> {
private int page;
private int size;
private int total;
// 시작 페이지 번호
private int start;
// 끝 페이지 번호
private int end;
// 이전 페이지의 존재 여부
private boolean prev;
// 다음 페이지의 존재 여부
private boolean next;
private List<E> dtoList;
}
✓ 제네릭을 이용하는 이유는 나중에 다른 종류의 객체를 이용해서 PageResponseDTO를 구성할 수 있도록 하기 위함 예를 들어 게시판이나 회원 정보 등도 페이징 처리가 필요할 수 있기 때문에 공통적인 처리를 위해서 제네릭으로 구성
✓ PageResponseDTO는 여러 정보를 생성자를 이용해서 받아서 처리하는 것이 안전 예를 들어 PageRequestDTO 에 있는 page, size 값이 필요하고, TodoDTO 목록 데이터와 전체 데이터의 개수도 필요
PageResponseDTO의 생성자는 Lombok의 @Builder를 적용
public class PageResponseDTO<E> {
@Builder(builderMethodName = "withAll")
public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total) {
log.info(pageRequestDTO);
this.page = pageRequestDTO.getPage();
this.size = pageRequestDTO.getSize();
this.total = total;
this.dtoList = dtoList;
}
1) 페이지 번호 계산
페이지 번호를 계산하려면 우선 현재 페이지의 번호 page 가 필요. 화면에 10개의 페이지 번호를 출력한다고 했을 때 다음과 같은 경우들이 생길수 있음.
현재 page가 1인 경우 : 시작 페이지 start는 1, 마지막 페이지 end는 10
현재 page가 10인 경우 : 시작 페이지 start는 1, 마지막 페이지 end는 10
현재 page가 11인 경우 : 시작 페이지 start는 11, 마지막 페이지 end는 20
A. 마지막 페이지 / 시작 페이지 번호의 계산
📍 end는 현재 페이지 번호를 기준으로 계산
this.end = (int)(Math.ceil(this.page/10.0)) * 10;
👾 page를 10으로 나눈 값을 올림처리 한 후 * 10 1 / 10 => 0.1 => 1 => 10 11 / 10.0 => 1.1 => 2 => 20 10 / 10 => 1.0 => 1 => 10
📍 시작 페이지 start의 경우 계산한 마지막 페이지에서 9를 빼면 됨
this.start = this.end - 9;
시작 페이지의 구성은 끝났지만 마지막 페이지의 경우다시 전체 개수 total를 고려해야 함. 만일 10개씩 (size) 보여주는 경우 전체 개수 total가 75라면 마지막 페이지는 10이 아니라 8이 되어야 함.
⚡️ list()는 @Valid를 이용해서 잘못된 파라미터 값들이 들어오는 경우 page는 1, size는 10으로 고정된 값을 처리하도록 구성 ⚡️ 기존과 달리 Model에 responseDTO라는 이름으로 PageResponseDTO를 담아 주었기 때문에 list.jsp를 수정
👾 화면에서 페이지 번호를 누르면 이동하는 처리는 자바스크립트를 이용해서 처리 👾 화면의 페이지 번호를 의미하는 <a> 태그에 직접 "onclick"을 적용할 수도 있지만, 한 번에 <ul> 태그에 이벤트를 이용해서 처리 👾 우선은 각 페이지 번호에 적절한 페이지 번호를 가지도록 구성. 이때는 "data-"속성을 이용해서 필요한 속성을 추가해주는 방식을 사용
⚡️ 자바 스크립트의 이벤트 처리는 <ul> 태그에 이벤트를 등록하고 <a> 태그를 클릭했을 때만 data-num 속성값을 읽어와서 현재 주소(self.location)를 변경하는 방식으로 작성 ⚡️ 자바 스크립트에서 백틱(`)을 이용하면 문자열 결합에 '+'를 이용해야 하는 불편함을 줄일 수 있음. 대신에 JSP의 EL이 아니라는 것을 표시하기 위하여 \${}로 처리 ⚡️ 자바 스크립트 처리가 완료되면 화면상의 페이지 번호를 클릭해서 페이지 이동이 가능한지 확인
D. 조회 페이지로 이동
목록 페이지는 특정한 Todo의 제목 title을 눌러서 조회 페이지로 이동하는 기능이 존재 기존에는 단순히 tno만을 전달해서 '/todo/read?tno=33'과 같은 방식으로 이동했지만, 페이지 번호가 붙을 때는 page와 size 등을 같이 전달해 주어야만 조회 페이지에서 다시 목록으로 이동할 때 기존 페이지을 볼 수 있게 됨 이를 위해 list.jsp에는 각 Todo의 링크 처리 부분을 수정할 필요가 있음
페이지 이동 정보는 PageRequestDTO 안에 있으므로 PageRequestDTO 내부에 간단한 메서드를 작성해서 필요한 링크를 생성해서 사용. (파라미터로 전달되는 PageRequestDTO는 Model로 자동 전달되기 때문에 별도의 처리가 필요하지 않음)
PageRequestDTO에 link라는 속성을 추가하고 getLink()를 추가해서 GET 방식으로 페이지 이동에 필요한 링크들을 생성
private String link;
public String getLink() {
if (link == null) {
StringBuilder builder = new StringBuilder();
builder.append("page=" + this.page);
builder.append("&size=" + this.size);
link = builder.toString();
}
return link;
}