1.  날짜와 시간 관련 위젯

날짜와 시간과 관련된 위젯으로는 타임피커, 데이트피커, 캘린더뷰, 크로노미터, 아날로그시계, 디지털시계 등이 있음

 

1)  아날로그시계와 디지털시계

 

🤓  아날로그 시계 AnalogClock와 디지털시계 DigitalClock는 화면에 시간을 표시하는 위젯으로 시계를 표현하는 용도로 쓰임. 이 둘은 View 클래스에서 상속받기 때문에 background 속성들을 설정할 수 있음. 디지털 시계는 textColor 같은 속성도 설정할 수 있음.

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

    <AnalogClock
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <DigitalClock
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center" />

</LinearLayout>

 


2)  크로노미터

 

크로노미터 Chronometer는 타이머 형식의 위젯이며 일반적으로 시간을 측정할 때 많이 사용
사용되는 메서드로는 start(), stop(), reset() 등이 있는데 이는 각각 크로노미터를 시작, 정지, 초기화.

 

android:format="시간 측정 : %s"
  ➡️ format 속성에서 타이머 앞의 문자열을 지정
<Chronometer
        android:id="@+id/chronometer1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:format="시간 측정 : %s"
        android:gravity="center"
        android:textSize="30dp" />

 


3) 타임피커, 데이트피커, 캘린더뷰


타임피커 TimePicker는 시간을, 데이트피커 DatePicker와 캘린더뷰 CalendarView는 날짜를 표시하고 조절하는 기능을

    ⚡️  캘린더뷰는 XML 속성이 여러가지인데, 그중에서 디폴트가 true인 showWeekNumber 속성은 현재 몇 주 차인지를 각주
의 맨 앞에 출력. 하지만 showWeekNumber 속성은 false로 하는 것이 더 깔끔하고 보기 좋음

롤리팝 API21 이후부터 타임피커와 데이트피커의 모양이 대폭 변경

 

  📍  이전 모양의 버전을 사용하려면
        타임피커는 android:timePickerMode="spinner" 속성을,
        데이트피커는 android:datePickerMode="spinner" 속성을 추가

    <TimePicker
        android:timePickerMode="spinner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <DatePicker
        android:datePickerMode="spinner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />


2.  기타 위젯

1)  자동완성텍스트뷰와 멀티자동완성텍스트뷰

 

자동완성텍스트뷰 AutoCompleteTextView와 멀티자동완성텍스트뷰 MultiAutoCompleteTextView는 텍스트뷰 보다는 에디트텍스트 속성이 더 강함.  사용자가 단어의 일부만 입력해도 자동 완성되는데, 자동완성텍스트뷰는 단어 1개가 자동 완성되고, 멀티자동완성텍스트뷰는 쉼표(,)로 구분하여 여러 개의 단어가 자동 완성.


  ✓  자동 완성 단어는 주로 Java 코드에서 배열로 설정하며 setAdapter() 메서드를 사용
  ✓  completionHint : XML 속성 줌 목록에 나타날 힌트

    completionThreshold : 자동 완성 시작되기 전에 사용자가입력해야 하는 최소 문자 수

   <AutoCompleteTextView
        android:id="@+id/autoCompleteTextView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:completionHint="선택하세요"
        android:completionThreshold="2"
        android:hint="자동완성텍스트뷰">
    </AutoCompleteTextView>

    <MultiAutoCompleteTextView
        android:id="@+id/multiAutoCompleteTextView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:completionHint="선택하세요"
        android:completionThreshold="2"
        android:hint="멀티자동완성텍스트뷰">
    </MultiAutoCompleteTextView>
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 속성이 주로 사용

 

   <ProgressBar style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        android:max="100"
        android:progress="20"
        android:secondaryProgress="50" />

    <SeekBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        android:progress="20" />

    <RatingBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"    
        android:layout_margin="20dp"
        android:numStars="5"
        android:rating="1.5"
        android:stepSize="0.5" />

 

style="?android:attr/progressBarStyleHorizontal"
> 프로그레스바의 모양을 지정. 이 스타일을 생략하면 원 모양이 나타남

    

android:max="100"
android:progress="20"
 > 프로그레스바의 범위를 최대 100으로 설정하고, 초깃값을 20으로 설정.

   

android:secondaryProgress="50"
> 프로그레스바의 보조 프로세스 초깃값을 50으로 설정. 메인 프로그레스보다 흐리게 표시.

    

android:numStars="5"
> 별의 개수를 설정. 디폴트는 5개.

 

android:rating="1.5"
> 초깃값을 설정. 1.5개가 채워진 상태로 표현.

 

android:stepSize="0.5"
> 한 스탭의 크기를 지정. 이 경우에는 별이 5개이므로 총 열 번의 스탭으로 구성.

 

 

 

 

 

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


1.  검색 / 필터링 조건의 정의

📍  대부분의 서비스에서는 검색 기능을 제공. 단순히 제목이나 내용 등을 검색하는 경우도 있고, 복잡한 검색 조건을 필터링 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 클래스에 새로운 메서드를 추가
@Test
    public void testSelectSearch() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(1)
                .size(10)
                .types(new String[]{"t", "w"})
                .keyword("AAAA")
                .build();
        List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
        voList.forEach(vo -> log.info(vo));
    }

 

📍 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'가 추가.

 

테스트 코드 수정해서 확인
@Test
    public void testSelectSearch() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(1)
                .size(10)
                .types(new String[]{"t", "w"})
                .keyword("스프링")
                .finished(true)
                .build();
        List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
        voList.forEach(vo -> log.info(vo));
    }

 

같은 방식으로 만료일 dueDate 을 처리하면 다음과 같음
<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()를 호출해서 결과를 확인
    @Test
    public void testSelectSearch() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(1)
                .size(10)
                .types(new String[]{"t", "w"})
                .keyword("스프링")
                //.finished(true)
                .from(LocalDate.parse("2022-04-28"))
                .to(LocalDate.parse("2024-04-30"))
                .build();
        List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
        voList.forEach(vo -> log.info(vo));

        log.info(todoMapper.getCount(pageRequestDTO));
    }

 

 

 

 

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


1.  목록 데이터를 위한 DTO와 서비스 계층

🚀  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이 되어야 함.

int last = (int)(Math.ceil(total/(double)size));

 

     123 / 10.0 => 12.3 => 13
     100 / 10.0 => 10.0 => 10
     75 / 10.0 => 7.5 => 8

 

📍 마지막 페이지 end는 앞에서 구한 last 값보다 큰 경우에 last 값이 end가 되어야만 함

this.end = end > last ? last : end;

B. 이전 prev / 다음 next 의 계산


이전 prev 페이지의 존재 여부는 시작 페이지 start가 1이 아니라면 무조건 true
다음 next은 마지막 페이지 end 와 페이지당 개수 size를 곱한 값보다 전체 개수 total가 더 많은지 보고 판단

this.prev = this.start > 1;
this.next = total > this.end * this.size;

 

PageResponseDTO는 최종적으로 Lombok의 @Getter를 적용


2)  TodoService / TodoServiceImpl

TodoService와 TodoServiceImpl에서는 작성된 PageResponseDTO를 반환타입으로 지정해서 getList()를 구성

public interface TodoService {
    void register(TodoDTO todoDTO);

    //List<TodoDTO> getAll();

    PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO);

    TodoDTO getOne(Long tno);

    void remove(Long tno);

    void modify(TodoDTO todoDTO);
}
 @Override
    public PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO) {
        List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
        List<TodoDTO> dtoList = new ArrayList<>();
        for(TodoVO todoVO : voList) {
            dtoList.add(modelMapper.map(todoVO, TodoDTO.class));
        }

        int total = todoMapper.getCount(pageRequestDTO);

        PageResponseDTO<TodoDTO> pageResponseDTO = PageResponseDTO.<TodoDTO>withAll()
                .dtoList(dtoList)
                .total(total)
                .pageRequestDTO(pageRequestDTO)
                .build();
        return pageResponseDTO;
    }

 

TodoService의 getList()는 테스트를 통해서 결과를 확인
  @Test
    public void pageingTest() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).build();
        PageResponseDTO<TodoDTO> pageResponseDTO = todoService.getList(pageRequestDTO);
        log.info(pageResponseDTO);
        for (TodoDTO todoDTO : pageResponseDTO.getDtoList()) {
            log.info(todoDTO);
        }
    }

 


3)  TodoController와 JSP처리

TodoController의 list()에서는 PageRequestDTO를 파라미터로 처리하고,

Model에 PageResponseDTO의 데이터를 담을 수 있도록 변경

@RequestMapping("/list")
    public void list(@Valid PageRequestDTO pageRequestDTO, BindingResult bindingResult, Model model) {
        log.info(pageRequestDTO);

        if(bindingResult.hasErrors()) {
            pageRequestDTO = pageRequestDTO.builder().build();
        }
        model.addAttribute("responseDTO", todoService.getList(pageRequestDTO));
    }

 

  ⚡️  list()는 @Valid를 이용해서 잘못된 파라미터 값들이 들어오는 경우 page는 1, size는 10으로 고정된 값을 처리하도록 구성
  ⚡️  기존과 달리 Model에 responseDTO라는 이름으로 PageResponseDTO를 담아 주었기 때문에 list.jsp를 수정

<c:forEach var="dto" items="${responseDTO.dtoList}">
    <tr>
        <th scope="row">${dto.tno}</th>
            <td><a href="/todo/read?tno=${dto.tno}" class="text-decoration-none">
                <c:out value="${dto.title}"/>
            </a></td>
            <td>${dto.writer}</td>
            <td>${dto.dueDate}</td>
            <td>${dto.finished}</td>
    </tr>
</c:forEach>

 

  ✓  프로젝트를 실행하고 브라우저에서 "/todo/list"를 호출했을 때 1페이지에 해당하는 데이터들이 출력되는 것을 확인


4)  페이지 이동 확인

화면을 추가로 개발하기 전에 "/todo/list?page=xx&size=xx"를 호출해서 결과가 정상적으로 처리되는지 확인.
page의 경우는 음수가 될 수 없고, size는 100을 넘을 수 없음.


A.  화면에 페이지 이동을 위한 번호 출력


브라우저를 통해서 페이지의 이동에 문제가 없다는 것을 확인했다면 화면 아래쪽에 페이지 번호들을 출력하도록 구성.
페이지 번호는 부트스트랩의 pagination이라는 컴포넌트를 적용.

 

list.jsp에 <table>태그가 끝난 후에 <div>를 구성해서 화면에 적용
<div class="float-end">
    <ul class="pagination flex-wrap">
        <c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
            <li class="page-item"><a class="page-link" href="#">${num}</a></li>
        </c:forEach>
    </ul>    
</div>

 

  ✓  브라우저에서 page를 변경시켜 페이지 번호들이 변경되는지 확인

 


B.  화면에서 prev / next / 현재 페이지


페이지 번호들이 정상적으로 출력된다면 "이전 / 다음"을 처리

<div class="float-end">
    <ul class="pagination flex-wrap">
        <c:if test="${responseDTO.prev}">
            <li class="page-item"><a class="page-link">Previous</a></li>
        </c:if>
        
        <c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
            <li class="page-item"><a class="page-link" href="#">${num}</a></li>
        </c:forEach>
                                
        <c:if test="${responseDTO.next}">
            <li class="page-item"><a class="page-link">Next</a></li>
        </c:if>
    </ul>
</div>

 

  ⚡️  11페이지 이상되었을 때 Previous 버튼이 보임

  ⚡️  1 ~ 10 페이지의 경우 Previous 버튼이 보이지 않음

📍 현재 페이지의 번호는 class 속성에 "active"라는 속성값이 추가되어야 함. 삼항 연산자를 이용

<c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
    <li class="page-item" ${responseDTO.page == num ? "active" : ""}>
    <a class="page-link" href="#">${num}</a></li>
</c:forEach>

 

 


C.  페이지의 이벤트 처리


👾  화면에서 페이지 번호를 누르면 이동하는 처리는 자바스크립트를 이용해서 처리
👾  화면의 페이지 번호를 의미하는 <a> 태그에 직접 "onclick"을 적용할 수도 있지만, 한 번에 <ul> 태그에 이벤트를 이용해서 처리
👾  우선은 각 페이지 번호에 적절한 페이지 번호를 가지도록 구성. 이때는 "data-"속성을 이용해서 필요한 속성을 추가해주는 방식을 사용

 

 "data-num"이라는 속성을 추가해서 페이지 번호를 보관하도록 구성
<c:if test="${responseDTO.prev}">
    <li class="page-item">
        <a class="page-link" data-num="${reponseDTO.start-1}">Previous</a>
    </li>
</c:if>

<c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
    <li class="page-item" ${responseDTO.page == num ? "active" : ""}>
    <a class="page-link" data-num="${num}">${num}</a></li>
</c:forEach>

<c:if test="${responseDTO.next}">
    <li class="page-item">
    <a class="page-link" data-num="${responseDTO.end+1}">Next</a></li>
</c:if>

 

<ul> 태그가 끝난 부분에 이벤트 처리 추가
 <script>
    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');
        self.location = `/todo/list?page=\${num}`; 
    });
</script>

 

  ⚡️  자바 스크립트의 이벤트 처리는 <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;
}

 

list.jsp 수정
<c:forEach var="dto" items="${responseDTO.dtoList}">
    <tr>
        <th scope="row">${dto.tno}</th>
        <td><a href="/todo/read?tno=${dto.tno}&${pageRequestDTO.link}" 
            class="text-decoration-none">
            <c:out value="${dto.title}"/>
        </a></td>
        <td>${dto.writer}</td>
        <td>${dto.dueDate}</td>
        <td>${dto.finished}</td>
    </tr>
</c:forEach>

 

  ⚡️ 기존의 tno=xx 뒤에 &를 추가하고 PageRequestDTO의 getLink()의 결과인 문자열을 생성하게 되면 다음과 같이 기존 링크에
page와 size가 추가된 형태가 됨

<a href="/todo/read?tno-3579&page=1&size=10" class="text-decoration-none" data-tno=357>

 


E. 조회에서 목록으로

 

조회 화면에서는 기존과 달리 PageRequestDTO를 추가로 이용하도록 TodoController를 수정

 

TodoController의 read 메소드는 PageRequestDTO 파라미터를 추가해서 수정
 @GetMapping({"/read", "/modify"})
    public void read(Long tno, PageRequestDTO pageRequestDTO, Model model) {
        TodoDTO todoDTO = todoService.getOne(tno);
        log.info(todoDTO);
        model.addAttribute("dto", todoDTO);
    }

 

read.jsp에서는 List 버튼의 링크를 다시 처리
document.querySelector('.btn-secondary').addEventListener('click', function (e) {
    self.location = "/todo/list?${pageRequestDTO.link}";
}, false);

 

  👾  브라우저는 특정한 페이지에서 조회 페이지로 이동해서 List 버튼을 눌렀을 때 정상적으로 이동하는지 확인

 


F.  조회에서 수정으로


조회 화면에서 수정 화면으로 이동할 때도 현재 페이지 정보를 유지해야 하기 때문에 read.jsp에서는 링크 처리 부분을 수정

document.querySelector('.btn-primary').addEventListener('click', function (e) {
    self.location = `/todo/modify?tno=${dto.tno}&${pageRequestDTO.link}`;
}, false);

 

  👾  화면에서 Modify 버튼을 누르면 아래와 같이 동작

 


G.  수정화면에서의 링크 처리


수정 화면에서도 다시 목록으로 가는 링크가 필요. TodoController의 read() 메서드는 GET 방식으로 동작하는 '/todo/modify'에도 동일하게 처리되므로 JSP에서 PageRequestDTO를 사용할 수 있음.

 

modify.jsp 의 List 버튼을 누르는 자바 스크립트의 이벤트 부분을 변경
document.querySelector('.btn-secondary').addEventListener('click', function (e) {
    self.location = `/todo/list?${pageRequestDTO.link}`;
});

 


H.  수정 / 삭제 처리 후 페이지 이동


실제 수정 / 삭제 작업은 POST 방식으로 처리되고 삭제 처리가 된 후에는 다시 목록으로 이동할 필요가 있음.
그렇기 때문에 수정 화면에서 <form> 태그로 데이터를 전송할 때 페이지와 관련된 정보를 같이 추가해서 전달해야 함.

 

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}">

 

 

TodoController에서 POST 방식으로 이루어지는 삭제 처리에도 PageRequestDTO를 이용해서 <form> 태그로 전송되는 태그들을 수집하고 수정 후에 목록 페이지로 이동할 때 page는 무조건 1페이지로 이동해서 size 정보를 활용

@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";
    }

 

  👾  브라우저는 목록에서 특정한 Todo를 조회 -> 수정 / 삭제 화면 -> 삭제 후 이동이 정상적으로 이루어지는지 확인

 


I.  수정 처리 후 이동


Todo를 수정한 후에 목록으로 이동할 때는 페이지 정보를 이용해야 하므로 TodoController의 modify() 에서는 PageRequestDTO를 받아서 처리하도록 변경.

    @PostMapping("/modify")
    public String midify(PageRequestDTO pageRequestDTO,
                         @Valid TodoDTO todoDTO,
                         BindingResult bindingResult,
                         RedirectAttributes redirectAttributes) {
        if (bindingResult.hasErrors()) {
            log.info("has error");
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            redirectAttributes.addAttribute("tno", todoDTO.getTno());
            return "redirect:/todo/mofidy";
        }
        log.info(todoDTO);
        todoService.modify(todoDTO);
        
        redirectAttributes.addAttribute("page", pageRequestDTO.getPage());
        redirectAttributes.addAttribute("size", pageRequestDTO.getSize());
        
        return "redirect:/todo/list";
    }

 

  👾  정상적으로 수정된 후에는 '/todo/list'로 이동할 때 필요한 page나 size를 유지할 수 있도록 구성

 

 

 

 

 

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


1.  페이징 처리를 위한 TodoMapper

👩🏻‍💻  Todo 데이터의 수가 많아진다면 데이터베이스에서 많은 시간이 걸릴 수 있고, 화면에서 이를 출력하는 데도 많은 시간과 자원이 소모
👩🏻‍💻  일반적으로는 많은 데이터를 보여주는 작업은 페이징 처리를 해서 최소한의 데이터들을 보여주는 방식을 선호
        ⚡️  페이징 처리를 하게 되면 데이터베이스에서 필요한 만큼의 최소한의 데이터를 가져오고 이를 출력하기 때문에 성능 개선에도 많은 도움이 됨

 

1)  페이징을 위한 SQL 연습

개발 전에 데이터베이스 상에서 원하는 동작을 미리 구현.


A. 더미 데이터 추가하기

 

많은 양의 데이터를 만드는 방법. 2배씩 늘어나기 때문에 몇번만 실행하면 많은 데이터를 생성함.
더미 데이터 추가를 위해 아래의 SQL을 여러번 처리.

insert into tbl_todo(title,dueDate,writer) (select title,dueDate,writer from tbl_todo);

 

  📍 최종적으로 데이터의 수를 확인

select count(tno) from tbl_todo;

B.  limit 실습


페이징 처리를 위해서는 select의 마지막 부분에는 limit 처리를 사용.
일반적으로 웹에서는 가장 최근에 등록된 데이터를 우선으로 보여주므로 가장 마지막에 등록된 데이터가 순차적으로 보이도록 쿼리를 작성

select * from tbl_todo order by tno desc;

 

📍  limit 뒤의 값이 한 개만 존재하는 경우 가져와야 하는 데이터의 수를 의미. 가장 최근 데이터 10개를 가져옴

select*from tbl_todo order by tno desc limit 10;
select*from tbl_todo order by tno desc limit 0, 10;

 


C.  count()의 필요성


페이징 처리를 하기 위해서는 페이지 번호를 구성할 때 필요한 전체 데이터의 개수가 필요함.
예를 들어 전체 데이터가 30개이면 3 페이지까지만 출력해야 하는 작업에서 사용

select count(tno) from tbl_todo;

 


2)  페이지 처리를 위한 DTO

💡  페이지 처리는 현재 페이지 번호 page, 한 페이지당 보여주는 데이터의 수 size가 기본적으로 필요

      ➡️  2개의 숫자를 매번 전달할 수도 있겠지만 나중에 확장 여부를 고려해서라도 별도의 DTO를 만들어 두는 것이 좋음

 

dto 패키지에 PageRequestDTO 클래스를 정의
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
    @Builder.Default
    @Min(value = 1)
    @Positive
    private int page = 1;

    @Builder.Default
    @Min(value=10)
    @Max(value=100)
    @Positive
    private int size = 10;

    public int getSkip() {
        return (page - 1) * size;
    }

}

 

  ✏️  페이지 번호 page와 한 페이지당 개수 size를 보관하는 용도 외에도 limit에서 사용하는 건너뛰기 skip 의 수를 getSkip() 만들어서 사용
  ✏️  page나 size는 기본값을 가지기 위해서 Lombok의 @Builder.Default를 이용
  ✏️  @Min, @Max를 이용해서 외부에서 조작하는 것에 대해서도 대비하도록 구성

 


3)  TodoMapper의 목록 처리

TodoMapper 인터페이스는 PageRequestDTO를 파라미터로 처리하는 selectList()를 추가

 List<TodoVO> selectList(PageRequestDTO pageRequestDTO);

 

TodoMapper.xml 내에서 selectList는 다음과 같이 구현
    <select id="selectList" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
        select * from tbl_todo order by tno desc limit #{skip}, #{size}
    </select>

 

  ✏️  MyBatis는 기본적으로 getXXX, setXXX를 통해서 동작하기 때문에 #{skip}의 경우는 getSkip()을 호출
 

테스트 코드를 이용해서 TodoMapper의 selectList()가 정상적으로 동작하는지 확인
 @Test
    public void testSelectList() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(5)
                .build();
        List<TodoVO> todoVOList = todoMapper.selectList(pageRequestDTO);
        for (TodoVO todoVO : todoVOList) {
            log.info(todoVO);
        }

        todoVOList.forEach(item -> log.info(item));

    }

 


4)  TodoMapper의 count 처리

화면에 페이지 번호들을 구성하기 위해서는 전체 데이터의 수를 알아야만 가능
  ✓ 예를 들어 마지막 페이지가 7에서 끝나야 하는 상황이 생긴다면 화면상에서도 페이지 번호를 조정해야 하기 때문

 

TodoMapper에 getCount()를 추가


  👾  getCount()는 나중에 검색에 대비해서 PageRequestDTO를 파라미터로 받로록 설계

 int getCount(PageRequestDTO pageRequestDTO);

 

TodoMapper.xml은 우선은 전체 개수를 반환하도록 구성
<select id="getCount" resultType="int">
    select COUNT(*) FROM tbl_todo
</select>

 

test 코드 실행 확인
 @Test
    public void testGetCount() {
        log.info(todoMapper.getCount(PageRequestDTO.builder().build()));
    }

 

 

 

 

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


1.  Todo의 삭제 기능 개발

수정과 삭제는 GET 방식으로 조회한 후에 POST 방식으로 처리
사실상 GET 방식의 내용은 조회 화면과 같지만 스프링 MVC에는 여러 개의 경로를 배열과 같은 표기법을 이용해서 하나의 @GetMapping으로 처리할 수 있기 때문에 read() 기능을 수정해서 수정과 삭제에도 같은 메서드를 이용하도록 작업

 

TodoController의 read()를 다음과 같이 수정해서 "/todo/modify?tno=xxx"의 경로를 처리하도록 수정
 @GetMapping({"/read", "/modify"})
    public void read(Long tno, Model model) {
        // 1) request로 전달 받은 tno를 서비스에 전달해서 2)TodoDTO를 반환받아서 3)View 에 전달
        TodoDTO todoDTO = todoService.getOne(tno);
        log.info(todoDTO);
        model.addAttribute("dto", todoDTO);
    }

 

 

/WEB-INF/view/todo 폴더에 있는 read.jsp를 그대로 복사해서 modify.jsp를 구성


  👾  modify.jsp에서는 수정과 삭제 작업이 POST 방식으로 처리될 예정이므로 이를 위한 <form> 태그를 구성하고 수정이 가능한 항목들은 편집이 가능하도록 함

<form action="/todo/modify" method="post">
    <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>
    <div class="input-group mb-3">
        <span class="input-group-text">Title</span>
        <input type="text" name="title" class="form-control" value="${dto.title}">
    </div>
    <div class="input-group mb-3">
        <span class="input-group-text">DueDate</span>
        <input type="date" name="dueDate" class="form-control" value="${dto.dueDate}">
    </div>
    <div class="input-group mb-3">
        <span class="input-group-text">Writer</span>
        <input type="text" name="writer" class="form-control" value="${dto.writer}" readonly>
    </div>
    <div class="form-check">
        <label class="form-check-label">
            Finished &nbsp;
        </label>
        <input type="checkbox" name="finished"
            class="form-check-input" ${dto.finished ? "checked" : ""}>
    </div>
    <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>

 

 

 

 ✓  제목 / 만료일 / 완료는 수정이 가능하도록 수정
  ✓  화면 아래 버튼은 삭제, 수정, 목록 버튼이 추가
  ✓  브라우저를 통해 "/todo/modify?tno=xx"와 같은 경로로 화면이 나오는 것 확인

 

 

 

 

 

 

 


 

1)  Remove 버튼의 처리


Remove 버튼의 클릭은 자바 스크립트를 이용해서 <form> 태그의 action을 조정하는 방식으로 동작하게 구성

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

 

  ✓  Remove 버튼은 class 속성이 "btn-danger"이므로 이를 이용해서 클릭 이벤트를 처리

 

TodoController에는 POST 방식으로 동작하는 remove() 메서드를 설계
    @PostMapping("/remove")
    public String remove(Long tno, RedirectAttributes redirectAttributes) {
        log.info("-----remove----");
        log.info("tno: " + tno);

        todoService.remove(tno);
        return "redirect:/todo/list";
    }

 

  ✓  우선 tno 파라미터가 정상적으로 전달되는지 확인하고 목록으로 이동하도록 구성

  ✓  Remove 버튼을 누르면 다음과 같은 로그 출력 확인


 

2) TodoMapper와 TodoService 처리

TodoMapper에는 delete() 메서드를 추가하고 TodoMapper.xml에는 sql을 추가
public interface TodoMapper {
    String getTime();

    void insert(TodoVO todoVO);

    List<TodoVO> selectAll();

    TodoVO selectOne(Long tno);

    void delete(Long tno);
}
    <delete id="delete">
        DELETE FROM tbl_todo WHERE tno = #{tno}
    </delete>

 

test 코드 작성
  @Test
    public void testDelete() {
        // 1) tno로 데이터를 반환해서 정상 출력 확인 2) 삭제 3) 다시 tno로 데이터를 반환해서 삭제 확인.
        Long tno = 2L;
        TodoVO todoVO = todoMapper.selectOne(tno);
        log.info(todoVO);

        todoMapper.delete(tno);

        todoVO = todoMapper.selectOne(tno);
        log.info(todoVO);

    }

 

TodoServce / TodoServiceImpl에는 remove() 메서드를 작성
public interface TodoService {
    void register(TodoDTO todoDTO);

    List<TodoDTO> getAll();

    TodoDTO getOne(Long tno);

    void remove(Long tno);
}
    @Override
    public void remove(Long tno) {
        todoMapper.delete(tno);
    }

 

 

TodoController 에서 TodoService의 remove()를 호출하는 코드를 추가
    @PostMapping("/remove")
    public String remove(Long tno, RedirectAttributes redirectAttributes) {
        log.info("-----remove----");
        log.info("tno: " + tno);

        todoService.remove(tno);
        return "redirect:/todo/list";
    }

 

  ✓  브라우저를 통해서 특정한 번호를 가진 게시물이 삭제 되는지 확인

 


2.  Todo의 수정 기능 개발

Todo의 수정 기능은 수정이 가능한 항목들만 변경되어야 하므로 SQL이 복잡해짐

 

TodoMapper에 메서드 추가
public interface TodoMapper {
    String getTime();

    void insert(TodoVO todoVO);

    List<TodoVO> selectAll();

    TodoVO selectOne(Long tno);

    void delete(Long tno);

    void update(TodoVO todoVO);
}

 

TodoMapper.xml 코드 추가
<update id="update">
    UPDATE tbl_todo SET title = #{title}, dueDate = #{dueDate}, 
        finished = #{finished} WHERE tno = #{tno}
</update>

 

test 코드 추가
@Test
    public void testUpdate() {
        Long tno = 12L;
        TodoVO todoVO = TodoVO.builder()
                .tno(tno)
                .title("수정")
                .dueDate(LocalDate.parse("2024-4-26"))
                .finished(true)
                .build();

        todoMapper.update(todoVO);
        log.info(todoMapper.selectOne(tno));
    }

 

 

TodoService / TodoServiceImpl 에서는 TodoDTO를 TodoVO로 변환해서 처리
public interface TodoService {
    void register(TodoDTO todoDTO);

    List<TodoDTO> getAll();

    TodoDTO getOne(Long tno);

    void remove(Long tno);

    void modify(TodoDTO todoDTO);
}
 @Override
    public void modify(TodoDTO todoDTO) {
        log.info("...modify()...");

        TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
        log.info(todoVO);
        todoMapper.update(todoVO);
    }

 


1)  checkbox를 위한 Formatter


👾  수정 작업에서는 화면에서 체크박스를 이용해서 완료여부 finished (boolean) 를 처리하게 됨
👾  문제는 브라우저가 체크박스가 클릭된 상태일때 전송되는 값은 "on"이라는 값을 전달
👾  TodoDTO로 데이터를 수집할 때에는 문자열 "on"을 boolean 타입으로 처리할 수 있어야 하므로 컨트롤러에서 데이터를 수집할 때 타입을 변경해 주기 위한 CheckboxFormatter 를 formatter 패키지에 추가해서 개발

public class CheckBoxFormatter implements Formatter<Boolean> {
    @Override
    public Boolean parse(String text, Locale locale) throws ParseException {
       if (text == null) return  false;
       return text.equals("on");
    }

    @Override
    public String print(Boolean object, Locale locale) {
        return object.toString();
    }
}

 

servlet-context.xml에 format 라이브러리 추가
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
        <set>
            <bean class="com.example.spring_ex_01_2404.controller.formatter.LocalDateFormatter"/>
            <bean class="com.example.spring_ex_01_2404.controller.formatter.CheckBoxFormatter"/>
        </set>
    </property>
</bean>

 


2)  TodoController의 modify()

@PostMapping("/modify")
    public String midify(@Valid TodoDTO todoDTO,
                         BindingResult bindingResult,
                         RedirectAttributes redirectAttributes) {
        if (bindingResult.hasErrors()) {
        // 유효성 검사 결과 에러가 있으면 수정 페이지로 돌아감
            log.info("has error");
            
            redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
            redirectAttributes.addAttribute("tno", todoDTO.getTno());
            return "redirect:/todo/mofidy";
        }
        log.info(todoDTO);
        todoService.modify(todoDTO);
        return "redirect:/todo/list";
    }

 

  ✓  @Valid를 이용해서 필요한 내용들을 검증하고 문제가 있는 경우에는 다시 "/todo/modify"로 이동시키는 방식을 사용
  ✓  "/todo/modify"로 이동할 때는 tno 파라미터가 필요하기 때문에 RedirectAttributes의 addAttribute을 이용

 

/WEB-INF/view/todo/modify.jsp 에 검증된 정보를 처리하는 코드를 추가


  👾  <form> 태그가 끝난 후에는 <script> 태그를 이용

<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 = '/todo/remove';
        frmModify.method = 'post';
        frmModify.submit();
    });
                            
    document.querySelector('.btn-primary').addEventListener('click', function () {
        frmModify.action = '/todo/modify';
        frmModify.method = 'post';
        frmModify.submit();
    });
                           
    document.querySelector('.btn-secondary').addEventListener('click', function () {
        self.location = '/todo/list';
    });
</script>

 

 

 

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


1.  Todo 목록 기능 개발

등록 기능의 개발이 완료된 후 결과 화면은 목록으로 이동
목록의 경우 나중에 페이징 처리나 검색 기능이 필요하지만 시작하는 단계에서는 목록 데이터를 출력하는 수준으로 작성

 

1) TodoMapper의 개발

TodoMapper 인터페이스에 가장 최근에 등록된 TodoVO가 우선적으로 나올 수 있도록 selectAll()를 추가
public interface TodoMapper {
    String getTime();

    void insert(TodoVO todoVO);

    List<TodoVO> selectAll();
}

 

 

TodoMapper.xml에서는 selectAll()의 실제 쿼리문을 작성


  👾  <select> 태그의 경우 resultType을 지정하는 것에 주의
          ➡️  resultType은 JDBC의 ResultSet의 한 행 row을 어떤 타입의 객체로 만들것인지를 지정

    <select id="selectAll" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
        SELECT * FROM tbl_todo ORDER BY tno DESC
    </select>

 

마지막으로 test 폴더 내에 작성해둔 TodoMapperTests 클래스를 이용해 테스트 코드를 작성
@Test
    public void testSelectAll() {
        List<TodoVO> todoVOList = todoMapper.selectAll();
        for (TodoVO todoVO : todoVOList) {
            log.info(todoVO);
        }

        todoVOList.forEach(item -> log.info(item));
        // for문과 foreach문 둘 중 하나 사용
    }

 

  ✓  테스트 실행 결과는 가장 나중에 추가된 데이터를 우선적으로 보여줌


2) TodoService / TodoServiceImpl의 개발

서비스 계층의 개발은 특별한 파라미터가 없는 경우 TodoMapper을 호출하는것이 전부이다. 다만, TodoMapper가 반환하는 데이터의 타입이 List<TodoVO>이기 때문에 이를 List<TodoDTO>로 변환하는 작업이 필요.

 

TodoService 인터페이스에 getAll() 기능을 추가
public interface TodoService {
    void register(TodoDTO todoDTO);

    List<TodoDTO> getAll();
}

 

TodoServiceImpl에서 getAll()을 작성

 

    @Override
    public List<TodoDTO> getAll() {
        List<TodoVO> voList = todoMapper.selectAll(); // dao에서 데이터베이스에서 들고온 VO리스트를 리턴
        List<TodoDTO> dtoList = new ArrayList<>();
        for (TodoVO todoVO: voList) {
            // 개별 VO를 DTO로 변환.
            TodoDTO todoDTO = modelMapper.map(todoVO, TodoDTO.class);
            dtoList.add(todoDTO); // DTO리스트에 저장.
        }
        return dtoList;
    }

 

💡  List<TodoVO>를 List<TodoDTO>로 변화하는 작업은 java8부터 지원하는 stream을 이용해서 각 TodoVO는 map()을 통해서 TodoDTO로 바꾸고, collect()를 이용해서 List로 묶어 줌

 

test 코드 작성후 실행
@Test
    public void testGetAll() {
        List<TodoDTO> todoDTOList = todoService.getAll();
        for (TodoDTO todoDTO: todoDTOList) {
            log.info(todoDTO);
        }

        todoDTOList.forEach(item -> log.info(item));
        // 둘 중 하나 사용
    }

 

 


3)  TodoController의 처리

TodoController의 list() 기능에서 TodoService를 처리하고 Model에 데이터를 담아서 JSP로 전달
@RequestMapping("/list")
    public void list(Model model) {
        log.info("todo list...");
        model.addAttribute("dtoList", todoService.getAll());
    }

 

  ✓  Model에는 dtoList라는 이름으로 목록 데이터를 담았기 때문에 JSP에서는 JSTL을 이용해서 목록을 출력

  ✓  화면 디자인은 부트스트랩의 tables 항목 참고

 

/WEB-INF/view/todo/list.jsp 페이지 생성

 

  • test.html 을 복사후 상단에 JSP, JSTL 설정 추가
  • <div class="card-body"> 부분을 아래와 같이 수정
<div class="card-body">
    <h5 class="card-title">Special title treatment</h5>
    <table class="table">
        <thead>
            <tr>
                <th scope="col">tno</th>
                <th scope="col">Title</th>
                <th scope="col">Writer</th>
                <th scope="col">DueDate</th>
                <th scope="col">Finished</th>
            </tr>
        </thead>
        <tbody>
        <c:forEach var="dto" items="${dtoList}">
            <tr>
                <th scope="row">${dto.tno}</th>
                <td>${dto.title}</td>
                <td>${dto.writer}</td>
                <td>${dto.dueDate}</td>
                <td>${dto.finished}</td>
            </tr>
        </c:forEach>
        </tbody>
    </table>
</div>

 


2.  Todo 조회 기능 개발

목록 화면에서 제목을 클릭했을때 "/todo/read?tno=xx"와 같이 TodoController를 호출하도록 개발

 

1)  TodoMapper 조회 기능 개발

 

TodoMapper의 개발은 selectOne()이라는 메소드를 추가


  👾  파라미터는 Long 타입으로 tno를 받도록 설계하고, TodoVO객체를 반환하도록 구성

public interface TodoMapper {
    String getTime();

    void insert(TodoVO todoVO);

    List<TodoVO> selectAll();

    TodoVO selectOne(Long tno);
}

 

TodoMapper.xml에 selectOne 추가
<select id="selectOne" resultType="com.example.spring_ex_01_2404.domain.TodoVO">
    SELECT * FROM tbl_todo WHERE tno = #{tno}
</select>

 

test 코드 작성
@Test
    public void testSelectOne() {
        TodoVO todoVO = todoMapper.selectOne(2L);
        log.info(todoVO);
    }

 


2)  TodoService / TodoServiceImpl의 개발

 

public interface TodoService {
    void register(TodoDTO todoDTO);

    List<TodoDTO> getAll();

    TodoDTO getOne(Long tno);
}
@Override
    public TodoDTO getOne(Long tno) {
        TodoVO todoVO = todoMapper.selectOne(tno);
        TodoDTO todoDTO = modelMapper.map(todoVO, TodoDTO.class);
        return todoDTO;
    }

 


3) TodoController의 개발


GET방식으로 동작하는 read() 기능을 개발

 @GetMapping("/read")
    public void read(Long tno, Model model) {
        // 1) request로 전달 받은 tno를 서비스에 전달해서 2)TodoDTO를 반환받아서 3)View 에 전달
        TodoDTO todoDTO = todoService.getOne(tno);
        log.info(todoDTO);
        model.addAttribute("dto", todoDTO);
    }

 

read.jsp 파일 생성
<div class="card-body">
    <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>
    <div class="input-group mb-3">
        <span class="input-group-text">Title</span>
        <input type="text" name="title" class="form-control" value="${dto.title}" readonly>
    </div>
    <div class="input-group mb-3">
        <span class="input-group-text">DueDate</span>
        <input type="date" name="dueDate" class="form-control" value="${dto.dueDate}" readonly>
    </div>
    <div class="input-group mb-3">
        <span class="input-group-text">Writer</span>
        <input type="text" name="writer" class="form-control" value="${dto.writer}" readonly>
    </div>
                        
    <div class="form-check">
       <label class="form-check-label">
           Finished &nbsp;
       </label>
       <input type="checkbox" name="finished" class="form-check-input" ${dto.finished ? "checked" : ""} disabled>
    </div>
    <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>
</div>


4)  수정 / 삭제를 위한 링크 처리


조회 화면에 수정 / 삭제를 위해서 Modify 버튼을 클릭하면 GET 방식의 수정 / 삭제 선택이 가능한 화면으로 이동
  ➡️  자바 스크립트를 이용해서 이벤트 처리

read.jsp 의 card-body 클래스 아래에 코드 작성
<script>
    document.querySelector('.btn-primary').addEventListener('click', function () {
        self.location = '/todo/modify?tno=' + ${dto.tno};
    });
    document.querySelector('.btn-secondary').addEventListener('click', function () {
        self.location = '/todo/list';
    })
</script>

 

list.jsp의 링크 처리
<tbody>
    <c:forEach var="dto" items="${responseDTO.dtoList}">
    <tr>
        <th scope="row">${dto.tno}</th>
        <td><a href="/todo/read?tno=${dto.tno}" class="text-decoration-none">
            ${dto.title}</a></td>
        <td>${dto.writer}</td>
        <td>${dto.dueDate}</td>
        <td>${dto.finished}</td>
    </tr>
    </c:forEach>
</tbody>

 

 

 

 

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

+ Recent posts