1.  메뉴

안드로이드의 메뉴는 옵션 메뉴 option menu와 컨텍스트 메뉴 context menu로구분

  ⚡️  옵션 메뉴를 사용하는 방법에는 메뉴 XML 파일을 생성한 후 Kotlin에서 호출하는 방법과 XML 파일 없이 Kotlin 코드만으로 메뉴를 생성하는 방법이 있음
  ⚡️  메뉴는 항목이 대부분 고정되어 있으므로 메뉴 XML 파일을 이용
        ➡️  메뉴 XML 파일을 이용하면 앱을 다른 나라 언어로 변경할 때 Kotlin코드를 건드리지 않아도 되므로 효율적

1)  XML을 이용한 옵션 메뉴

메뉴 XML 파일을 이용하는 방식은 세 가지만 설정하면 됨

  • 메뉴 코딩 : 메뉴 폴더 생성 및 메뉴 XML 파일 생성, 편집
  • 메뉴 파일 등록 : Kotlin 코딩. onCreateOptionsMenu() 메서드 오버라이딩
  • 메뉴 선택 시 동작할 내용 코딩 : Kotlin 코딩. onOptionsItemSelected() 메서드 오버라이딩
메뉴 XML 파일
<menu>
    <item
        android:id="@+id:항목1아이디"
        android:title="항목1 제목" />
    <item
        android:id="@+id:항목2아이디"
        android:title="항목2 제목" />
</menu>

 

  ✓ 위의 예에서 항목 item 이 2개 ➡️  메뉴에는 '항목1 제목', '항목2 제목'이 출력됨. 또한 메뉴 안에 서브 메뉴도 생성할 수 있음

 

Activity 클래스에서 오버라이딩하는 onCreateOptionsMenu() 메서드

 

    ✓  앱이 실행되면 메뉴의 내용을 XML 파일에서 자동으로 읽어옴. 메서드에 코딩할 내용은 거의 고정화 되어 있음

  override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.메뉴XML아이디, menu)
        return true
    }

 

Activity 클래스에서 오버라이딩하는 onOptionsItemSelected() 메서드 

 

    ✓  메뉴를 선택했을 때 어떤 동작을 할 것인지 담고 있음. 이 메서드에 실제 동작할 내용을 코딩하면 됨
    ✓  메뉴는 항목이 여러 개 나오기 때문에 보통 메서드 내부에서 when 을 사용

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when(item.itemId) {
        R.id.항목1아이디 -> {
            항목 1을 선택했을 때 실행되는 코드
        }
        R.id.항목2아이디 -> {
            항목 2을 선택했을 때 실행되는 코드
        }
    }    
    return super.onOptionsItemSelected(item)
}

 

 

 

인플레이터 Inflater


정적으로 존재하는 XML 파일을 Kotlin 코드에서 접근해서 실제 객체로 만들어 사용하는 것
메뉴 인플레이터 MenuInflater 객체는 메뉴 XML 파일을 Kotlin 코드에서 가져와 사용

레이아웃 인플레이터 LayoutInflater 객체는 레이아웃 XML 파일을 Kotlin 코드에서 가져와 사용하는 것

 


 

2)  Kotlin 코드만 이용한 옵션 메뉴

onCreateOptionsMenu() 메서드 안에 메뉴 XML 파일에 접근하는 대신에 직접 munu.add() 메서드로 메뉴항목을 추가
onOptionsItemSelected() 메소드의 case문을 코드에서 지정한 항목의 아이디 순번으로 변경하면 됨

class MainActivity : AppCompatActivity() {

    lateinit var baseLayout: LinearLayout
    lateinit var button: Button
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        title = "배경색 바꾸기"

        baseLayout = findViewById(R.id.baseLayout)
        button = findViewById(R.id.button1)

    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        super.onCreateOptionsMenu(menu)

        menu?.add(0, 1, 0, "배경색 (빨강)")
        menu?.add(0, 2, 0, "배경색 (초록)")
        menu?.add(0, 3, 0, "배경색 (파랑)")

        val subMenu = menu?.addSubMenu("버튼 변경 >> ")
        subMenu?.add(0, 4, 0, "버튼 45도 회전")
        subMenu?.add(0, 5, 0, "버튼 2배 확대")
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId) {
            1 -> {
                baseLayout.setBackgroundColor(Color.RED)
            }
            2 -> {
                baseLayout.setBackgroundColor(Color.GREEN)
            }
            3 -> {
                baseLayout.setBackgroundColor(Color.BLUE)
            }
            4 -> {
                button.rotation = 45f
            }
            5 -> {
                button.scaleX = 2f
            }
        }
        return super.onOptionsItemSelected(item)
    }
}

 

    ✓  add() 메서드의 파라미터는 그룹 아이디, 항목아이디, 순번, 제목순으로 지정
          ➡️  두 번째 지정한 아이디가 onOptionsItemSelected()의 when 과 같으면 됨

 

 

 


3)  XML을 이용한 컨텍스트 메뉴

🚀  옵션 메뉴는 키패드의 메뉴 버튼을 클릭할 때 나타나는 것과 달리 컨텍스트 메뉴는 레이아웃 또는 버튼, 에디트텍스트 등의 위젯을 롱클릭했을 때 나타남
🚀  컨텍스트 메뉴에서 메뉴 XML파일을 이용하는 방식은 옵션 메뉴와 비슷. 단, 여러 개의 위젯에 메뉴를 설정할 수 있으므로 onCreate 메서드에서 컨텍스트 메뉴을 나타낼 위젯을 registerForContextMenu()로 등록해야 함. 또, 옵션 메뉴에서 사용한 메서드와 메서드 이름이 약간 다름

  • 메뉴 코딩 : 메뉴 폴더 생성 및 위젯의 메뉴 XML 파일 생성. 편집
  • 메뉴를 사용할 위젯 등록 : Kotlin 코딩. onCreate() 안에 registerForContextMenu()로 등록
  • 메뉴 파일 등록 : Kotlin 코딩. onCreateContextMenu() 메서드 오버라이딩
  • 메뉴 선택시 동작할 내용 코딩 : Kotlin 코딩. onContextItemSelected() 메서드 오버라이딩
@Override 
public void onCreateContextMenu(ContextMenu menu, 
    View v, 
    ContextMenu.ContextMenuInfo menuInfo) { 
    
        super.onCreateContextMenu(menu, v, menuInfo);  
        
        MenuInflater menuInflater = getMenuInflater(); 
         
            if (v == 위젯1) { 
                menuInflater.inflate(R.menu.첫번째메뉴XML파일, menu); 
            } 
            if (v == 위젯2) { 
                menuInflater.inflate(R.menu.두번째메뉴XML파일, menu); 
            }
}

 

 

 

 

 

 

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


 

🚀  SD 카드 특정 폴더의 이미지 파일을 보여주는 간단한 이미지 뷰어 앱 만들기

 

1.  화면 디자인 및 편집

 

1) MyPictureView 클래스

커스텀 위젯 (Custom Widget, Custom View)을 직접 만들어서 activity_main.xml에 넣어서 사용.
  ➡️ 커스텀 위젯은 지정된 이미지 파일을 출력하는 역할

 

MyPictureView.kt

 

    ✓  onDraw() 메서드를 오버라이딩

class MyPictureView(context: Context, attrs: AttributeSet?) : View(context, attrs) {

    var imagePath : String? = null

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        try {
            if (imagePath != null) {
                val bitmap = BitmapFactory.decodeFile(imagePath)
                canvas.scale(2f, 2f, 0f, 0f)
                canvas.drawBitmap(bitmap!!, 0f, 0f, null)
                bitmap.recycle()
            }
        } catch ( e : Exception) {

        }
    }
}

 

var imagePath : String? = null


    ✓  이미지 파일의 경로 및 파일 이름을 저장할 변수

 

if (imagePath != null) {
val bitmap = BitmapFactory.decodeFile(imagePath)
canvas.scale(2f, 2f, 0f, 0f)
canvas.drawBitmap(bitmap!!, 0f, 0f, null)
bitmap.recycle()
}


    ✓ imagePath에 값이 있으면(경로 및 파일이름이 지정되었다면) 화면에 그림 파일을 출력

 


 

2) activity_main.xml

  • 가로 레이아웃에 버튼 2개를 생성
  • 커스텀 위젯인 MyPictureView를 생성
  • 위젯의 이름은 btnPrev, btnNext, myPictureView
<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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <Button
            android:id="@+id/btnPrev"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="이전 그림" />

        <Button
            android:id="@+id/btnNext"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="다음 그림" />

    </LinearLayout>

    <kr.abc.stream_practice.MyPictureView
        android:id="@+id/myPictureView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

 

 

👾  /storage/emulated/0/Pictures 에 이미지 업로드 후 AndroidManifest.xml 에 권한 설정

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

 


 

2.  kotlin 코드 작성 및 수정

class MainActivity : AppCompatActivity() {
    // 전역변수 선언
    lateinit var btnPrev: Button
    lateinit var btnNext: Button
    lateinit var myPicture: MyPictureView
    
    var curIndex: Int = 1 // 이미지 파일의 인덱스로 사용할 변수
    var imageFiles: Array<File>? = null // SD 카드에서 읽어올 이미지 파일의 배열

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

        title = "간단 이미지 뷰어"
        
        // 접근 권한 요청
        ActivityCompat.requestPermissions(
            this,
            arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
            Context.MODE_PRIVATE
        )

        btnPrev = findViewById(R.id.btnPrev)
        btnNext = findViewById(R.id.btnNext)
        myPicture = findViewById(R.id.myPictureView)

        imageFiles =
            File(Environment.getExternalStorageDirectory().absolutePath + "/Pictures").listFiles()

        // 파일 목록 출력
        for (i in imageFiles!!.indices) {
            var fileName = if (imageFiles!![i].isDirectory == true)
                "<폴더> " + imageFiles!![i].toString()
            else
                "<파일> " + imageFiles!![i].toString()
            println(fileName)
        }

        // 첫 번째 파일을 커스텀 위젯에 출력
        // 해당 인덱스의 이미지 파일 이름을 myPicture에 전달한다는 뜻
        myPicture.imagePath = imageFiles!![curIndex].toString()

        btnPrev.setOnClickListener {
            if (curIndex <= 1) {
                Toast.makeText(applicationContext, "첫번째 그림입니다", Toast.LENGTH_SHORT).show()
            } else {
                myPicture.imagePath = imageFiles!![--curIndex].toString()
                myPicture.invalidate()
            }
        }

        btnNext.setOnClickListener {
            if (curIndex >= imageFiles!!.size-1) {
                Toast.makeText(applicationContext, "마지막 그림입니다.", Toast.LENGTH_SHORT).show()
            } else {
                myPicture.imagePath = imageFiles!![++curIndex].toString()
                myPicture.invalidate()
            }
        }
    }
}

 

 


첫 번째 이미지에서 이전 버튼을 누르면 마지막 이미지가 뜨거나,
마지막 이미지에서 다음 버튼을 누르면 첫 번째 이미지가 뜨게 하기
class MainActivity : AppCompatActivity() {
    lateinit var btnPrev: Button
    lateinit var btnNext: Button
    lateinit var textIndex: TextView
    lateinit var textTotal: TextView
    lateinit var myPicture: MyPictureView
    var curIndex: Int = 1
    var imageFiles: Array<File>? = null

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

        title = "간단 이미지 뷰어"
        ActivityCompat.requestPermissions(
            this,
            arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
            Context.MODE_PRIVATE
        )

        btnPrev = findViewById(R.id.btnPrev)
        btnNext = findViewById(R.id.btnNext)

        textIndex = findViewById(R.id.textIndex)
        textTotal = findViewById(R.id.textTotal)

        myPicture = findViewById(R.id.myPictureView)

        imageFiles =
            File(Environment.getExternalStorageDirectory().absolutePath + "/Pictures").listFiles()

        for (i in imageFiles!!.indices) {
            var fileName = if (imageFiles!![i].isDirectory == true)
                "<폴더> " + imageFiles!![i].toString()
            else
                "<파일> " + imageFiles!![i].toString()
            println(fileName)
        }

        myPicture.imagePath = imageFiles!![curIndex].toString()
        textTotal.text = (imageFiles!!.size-1).toString()
        textIndex.text = curIndex.toString()

        btnPrev.setOnClickListener {

            if (curIndex == 1) {
                curIndex = imageFiles!!.size
            } else {
                myPicture.imagePath = imageFiles!![--curIndex].toString()
                textIndex.text = curIndex.toString()
                myPicture.invalidate()
            }

        }

        btnNext.setOnClickListener {

            if (curIndex >= imageFiles!!.size-1) {
               curIndex = 0
            } else {
                myPicture.imagePath = imageFiles!![++curIndex].toString()
                textIndex.text = curIndex.toString()
                myPicture.invalidate()
            }

        }

    }
}

 

 

 

 

 

 

 

 

 

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


✏️  일기장 앱을 내장 메모리가 아닌 sd카드의 mydiary 폴더에 저장되도록 작업. 단, SD카드에 mydiary가 없으면 kotlin 코드에서 자동 생성되게 함

 

xml 코드
<?xml version="1.0" encoding="utf-8"?>
<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="20dp"
        android:background="#E7D9F6"
        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>

 

 

MainActivity 코드
class MainActivity : AppCompatActivity() {
    lateinit var datePicker: DatePicker
    lateinit var editText: EditText
    lateinit var btn: Button
    lateinit var fileName: String 

    /* 추가된 코드 */
    lateinit var savePath: String // 저장 경로
    var isFlag = false // 디렉토리 생성을 저장

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

        title = "간단 일기장"

        // 접근 권한 요청
        ActivityCompat.requestPermissions(this,
            arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
            Context.MODE_PRIVATE)

        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)
        
        // 외부 경로 지정
        savePath = Environment.getExternalStorageDirectory().absolutePath + "/MyDiary"

        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 ->
                makeDir() // 새로운 디렉토리 생성 함수 ( 있으면 다시 생성 x )
                
                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 = FileOutputStream("$savePath/$fileName") // 지정 경로 이름으로 저장
            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 = FileInputStream("${savePath}/${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;
    }

    private fun makeDir() {
        if (!isFlag) { // 외부에 디렉토리가 생성되어 있지 않으면 생성
            // 저장 경로 생성
            val myDir = File(savePath)
            myDir.mkdir()
            isFlag = true
        }
    }

}

 

 

 

 

 

 

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


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 학원 강의 ]

+ Recent posts