이제 본격적으로 악보를 추가하고, 현재 DB에 존재하는 악보를 조회하는 기능을 추가하고자 한다.
2번째 포스팅에서 테스트했던 내용을 실제 기능으로 옮기는 것으로 볼 수 있다.
우선 데이터를 추가하기 위해 상단 액션바에 검색버튼과 추가 버튼을 넣어주었다.
검색 아이콘과 추가 버튼 아이콘을 어떻게 가져올까 고민하다가
검색의 경우 안드로이드에서 기본으로 제공하는 기능을 쓰기로 했다.
팔레트에 이렇게 'Search Item' 이 있다.
이 아이템을 활용하면 검색 아이콘이 자동으로 만들어지고,
검색 아이콘을 클릭하면 상단바에 검색창이 생성된다.
검색창에 검색어를 입력하고 키보드의 돋보기 아이콘을 누르면 검색이 된다.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/tab_sheet_top_search"
android:icon="@drawable/ic_search_black_24dp"
android:title="Search"
app:showAsAction="always"
app:actionViewClass="android.widget.SearchView" />
<item
android:id="@+id/tab_sheet_top_add"
android:icon="@drawable/ic_add_new_24dp"
android:title="New"
app:showAsAction="always"/>
</menu>
상단 액션바의 메뉴 리소스로 들어간 xml 코드이다.
아이콘의 경우 안드로이드에서 기본으로 제공하는 아이콘을 활용했다.
아이콘 이미지를 찾아 넣었는데, 사이즈가 안맞아서, 사이즈 조정을 어떻게 하는지 검색하다가
다음 그림처럼 drawable에 Image Asset을 추가하면 사이즈 조절까지 된다는 글을 찾았다.
그러다 문득 기존 아이콘이 벡터 이미지라는 것을 알고 하단의 Vector Asset을 쓰면 어떨지 궁금해졌다.
Vector Asset을 클릭하면 다음과 같은 창이 열린다.
여기에서 Clip Art 를 클릭하면
다음 사진처럼 안드로이드에서 기본으로 제공하는 아이콘들이 나타난다.
(심지어 포켓몬 아이콘도 있다)
어지간히 있을 건 다 있어서 이쪽에서 골라쓰면 편하다.
+ 아이콘도 이곳에서 골라 적용했다.
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_tab_sheet, menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int select = item.getItemId();
switch (select) {
case R.id.tab_sheet_top_add:
AlertDialog.Builder builder = new AlertDialog.Builder(context);
View dialogView = getLayoutInflater().inflate(R.layout.dialog_new_sheet, null);
builder.setView(dialogView);
builder.setTitle("새로운 악보");
EditText edt_song_name = dialogView.findViewById(R.id.edt_song_name);
EditText edt_singer = dialogView.findViewById(R.id.edt_singer);
builder.setPositiveButton("취소", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
builder.setNegativeButton("확인", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent NewSheetIntent = new Intent(context, NewSheetActivity.class);
NewSheetIntent.putExtra("song_name", edt_song_name.getText().toString());
NewSheetIntent.putExtra("singer", edt_singer.getText().toString());
startActivity(NewSheetIntent);
}
});
AlertDialog dialog_new_sheet = builder.create();
dialog_new_sheet.show();
return true;
case R.id.tab_sheet_top_search:
return true;
}
return false;
}
메뉴에 대한 코드는 다음과 같다.
우선 검색버튼은 안드로이드에서 제공하는 class와 제공 메소드를 공부하고 써야해서
공부를 하고 채우기 위해 비워두었다.
+ 버튼을 눌렀을 때는 AlertDialog를 띄워서 곡 정보를 간단하게 기입하도록 했다.
데이터 추가/조회의 경우 이 정보를 토대로 테스트하기로 했다.
다이어로그의 View는 커스텀으로 만들어 인플레이트했다.
다이어로그의 뷰 디자인은 간단하게 다음처럼 했다.
자바 코드에서 볼 수 있듯, 확인버튼을 누르면
인텐트에 곡이름과 가수 정보를 담아서 새 액티비티를 만들어 넘기도록 했다.
새로운 액티비티에서는 제대로 코드 악보를 만들도록 할 것이다.
(아직 구현은 안했다)
생각해보니 곡 이름은 not null 이기 때문에 반드시 입력해야 하는 값이다.
이 값이 입력되지 않은 채로 확인을 눌렀을 때
안내 메세지를 띄우도록 기능을 추가해야한다.
테스트로 값을 넣어서 확인을 눌러보면 다음과 같은 창이 뜬다.
'너와 나' 인지 '너랑 나'인지 헷갈렸지만..
아마 '너랑 나'였던 걸로 기억하면서 테스트로 넣어보았다.
확인버튼을 누르면 다음과 같은 새로운 액티비티를 띄우도록 했다.
상단 액션바에 체크버튼을 추가했다.
편집을 완료하면 해당 버튼을 눌러 데이터를 추가할 수 있도록 한다.
이 체크버튼에는 다음과 같은 코드를 넣었다.
package com.everdu.chordshare;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class NewSheetActivity extends AppCompatActivity {
private static String IP_ADDRESS = "서버IP주소";
private static String TAG = "Test";
private static String INSERT_PHP = "사용하는 php파일명";
ActionBar actionBar;
String song_name;
String singer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_new_sheet);
Intent getIntent = getIntent();
song_name = getIntent.getStringExtra("song_name");
singer = getIntent.getStringExtra("singer");
actionBar = getSupportActionBar();
actionBar.setTitle("새로운 악보");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_activity_new_sheet, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int select = item.getItemId();
switch (select) {
case R.id.new_sheet_check:
InsertData insertData = new InsertData();
insertData.execute("http://" + IP_ADDRESS + "/" + INSERT_PHP, song_name, singer);
return true;
}
return false;
}
class InsertData extends AsyncTask<String, Void, String> {
ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
super.onPreExecute();
progressDialog = ProgressDialog.show(NewSheetActivity.this, "plz wait", null, true, true);
}
@Override
protected String doInBackground(String... params) {
// execute 메소드 실행시 들어간 인자들이 params에 할당된다.
String serverURL = (String)params[0];
String song_name = (String)params[1];
String singer = (String)params[2];
String postParameters = "song_name=" + song_name + "&singer=" + singer;
try {
URL url = new URL(serverURL);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setReadTimeout(5000);
httpURLConnection.setConnectTimeout(5000);
httpURLConnection.setRequestMethod("POST");
httpURLConnection.connect();
// 웹 서버에 요청 전송
OutputStream outputStream = httpURLConnection.getOutputStream();
outputStream.write(postParameters.getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
// 웹 서버로부터 응답 수신
InputStream inputStream;
int responseStatusCode = httpURLConnection.getResponseCode();
if (responseStatusCode == HttpURLConnection.HTTP_OK) {
//연결이 잘 되었으면, input stream을 얻는다.
inputStream = httpURLConnection.getInputStream();
}
else {
inputStream = httpURLConnection.getErrorStream();
}
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
StringBuilder stringBuilder = new StringBuilder();
String line = null;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
bufferedReader.close();
return stringBuilder.toString();
} catch (Exception e) {
return new String("Error : " + e.getMessage());
}
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
progressDialog.dismiss();
// 확인 메세지
AlertDialog.Builder builder = new AlertDialog.Builder(NewSheetActivity.this);
builder.setMessage(s);
builder.setPositiveButton("확인", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
NewSheetActivity.this.finish();
}
});
builder.create().show();
}
}
}
프래그먼트로는 getActivity, getContext를 사용할 수 없어
onAttach 메소드로 Context를 가져와 사용한다.
확인버튼을 누르면 AsyncTask를 상속받는 내부클래스 객체를 생성하여
해당 객체의 execute메소드를 실행해 웹서버에 연결한 후 php코드를 실행시켜 Insert 쿼리를 실행한다.
그리고 insert 쿼리의 결과를 받아 결과를 다이어로그로 띄워준다.
php코드는 다음과 같이 작성하였다.
<?php
error_reporting(E_ALL);
ini_set('display_errors',1);
include('DbConn.php');
$android = strpos($_SERVER['HTTP_USER_AGENT'], "Android");
if( (($_SERVER['REQUEST_METHOD'] == 'POST') && isset($_POST['submit'])) || $android )
{
// 안드로이드 코드의 postParameters 변수에 적어준 이름을 가지고 값을 전달 받습니다.
$song_name =$_POST['song_name'];
$singer =$_POST['singer'];
if(empty($song_name)){
$errMSG = "곡 이름을 입력하세요.";
}
if(!isset($errMSG)) // 이름과 나라 모두 입력이 되었다면
{
try{
// SQL문을 실행하여 데이터를 MySQL 서버의 person 테이블에 저장합니다.
$stmt = $con->prepare('
INSERT INTO sheet_list (sheet_nm, singer)
VALUES (:sheet_nm
, :singer)');
$stmt->bindParam(':sheet_nm', $song_name);
$stmt->bindParam(':singer', $singer);
if($stmt->execute())
{
$successMSG = "새로운 악보를 추가했습니다.";
$successMSG = iconv("euckr", "utf-8", $successMSG);
}
else
{
$errMSG = "사용자 추가 에러";
$successMSG = iconv("euckr", "utf-8", $errMSG);
}
} catch(PDOException $e) {
die("Database error: " . $e->getMessage());
}
}
}
?>
<?php
if (isset($errMSG)) echo $errMSG;
if (isset($successMSG)) echo $successMSG;
?>
필요없는 코드도 좀 있지만 중요한 기능은 잘 수행한다.
처음에 insert를 시도했을 때는 몇가지 오류가 떠서 당황했었는데,
첫번째는 잘 됐었던 http연결이 안되었던 것이다.
오류 메세지를 토스트로 띄워보니
"cleartext http traffic to not permitted" 라는 오류가 떴었다.
구글 검색으로 메니페스트파일에 코드를 한줄 추가하여 쉽게 해결했다.
안드로이드 버전이 높은 기종에서 발생하는 오류라고 한다.
(분명 2번째 포스팅에서 테스트할 때는 없었던 오류인데 왜 이제서야 나오는지 모르겠다)
두번째로 http연결 후 404 오류가 뜨는 문제가 발생했는데,
이상하게 생각해서 웹으로 접속해보니 웹으로도 404오류가 발생했다.
오류의 요인은 간단했는데, 오류가 발생한 이유는 지금도 잘 모르겠다..
바로 php파일명에 '_' 문자를 썼기 때문이었다.
기존에 잘 됐던 파일명과 내가 작성한 파일명을 보고 그냥 혹시나 하는 마음에 바꿔봤는데
이렇게 해결되어버렸다 ㅋㅋ
신기해서 구글링을 좀 해봤는데 정보가 거의 없었다.
스택오버플로우 글을 하나 찾았는데
오히려 써도 괜찮다는 답변을 보니 더 이상했다...
URL 접속할 때 인코딩 문제인가...
일단 우여곡절끝에 insert에는 성공을 했지만
아직 에러처리에 대한 부분이 많이 부족해서 다양한 에러 상황에도 대처가 가능하도록 손을 보아야한다.
이 부분이 개인적으로 제일 어려운 부분이라고 생각한다...
그래서 알고리즘 문제를 풀 때 스스로 예외를 찾는 훈련을 시키는 걸지도 모른다ㅋㅋ
insert 후에는 확인 메세지를 띄우고,
확인버튼을 누르면 현재 액티비티를 종료한다.
PuTTY로 서버에 접속해 DB를 살펴보면 다음과 같이 데이터가 잘 추가된 것을 볼 수 있다.
잘 안보이지만
not null로 설정된 다른 값들은 모두 디폴트값을 넣어두었다.
다음과 같이 잘 들어가 있다.
이제 이렇게 DB에 들어있는 데이터를 조회해서 리사이클러 뷰로 메인에 띄워주는 코드를 작성한다.
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_frag_music_sheet, container, false);
setHasOptionsMenu(true);
initUI(rootView);
return rootView;
}
private void initUI(ViewGroup rootView) {
recyclerView = rootView.findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
recyclerView.setLayoutManager(layoutManager);
SelectData selectData = new SelectData();
selectData.execute("http://" + IP_ADDRESS + "/" + SELECT_PHP, "");
}
프래그먼트가 생성될 때 initUI 함수를 실행하고
함수안에서 직접 정의한 SelectData 객체를 만들어 execute 시켰다.
Insert 때와 구조는 비슷하다.
데이터는 JSON 형태로 가져와서 파싱하여 쓰도록 했다.
안드로이드에 JSON 관련 클래스가 구비되어있어
JSON객체로부터 정보를 가져오는 것은 어렵지 않았다.
class SelectData extends AsyncTask<String, Void, String> {
ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
super.onPreExecute();
progressDialog = ProgressDialog.show(context, "plz wait", null, true, true);
}
@Override
protected String doInBackground(String... params) {
// execute 메소드 실행시 들어간 인자들이 params에 할당된다.
String serverURL = (String)params[0];
String query = (String)params[1];
String postParameters = "query=" + query;
try {
URL url = new URL(serverURL);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setReadTimeout(5000);
httpURLConnection.setConnectTimeout(5000);
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setDoInput(true);
httpURLConnection.connect();
// 웹 서버에 요청 전송
OutputStream outputStream = httpURLConnection.getOutputStream();
outputStream.write(postParameters.getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
// 웹 서버로부터 응답 수신
InputStream inputStream;
int responseStatusCode = httpURLConnection.getResponseCode();
if (responseStatusCode == HttpURLConnection.HTTP_OK) {
inputStream = httpURLConnection.getInputStream();
}
else {
inputStream = httpURLConnection.getErrorStream();
}
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
StringBuilder stringBuilder = new StringBuilder();
String line = null;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
bufferedReader.close();
return stringBuilder.toString().trim();
} catch (Exception e) {
String errMsg = new String("Error : " + e.getMessage());
return null;
}
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
progressDialog.dismiss();
if (s == null) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage("조회 중 에러가 발생했습니다.");
builder.create().show();
}
else {
try {
JSONObject jsonObject = new JSONObject(s);
JSONArray jsonArray = jsonObject.getJSONArray(TAG_JSON);
adapter = new ItemSheetAdapter();
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject item = jsonArray.getJSONObject(i);
String sheet_id = item.getString(TAG_SHEET_ID);
String song_name = item.getString(TAG_SONG_NAME);
String singer = item.getString(TAG_SINGER);
adapter.addItem(new ItemSheetList(sheet_id, song_name, singer));
}
recyclerView.setAdapter(adapter);
}
catch (Exception e) {
}
}
}
}
}
SelectData 클래스를 다음과 같이 만들었다.
JSON 객체배열로부터 객체를 하나씩 가져오면서
동시에 리사이클러 뷰 adapter에 아이템을 추가하도록 하였다.
현재 이 조회쿼리는 프래그먼트가 최초로 실행되었을 때만 작동하기 때문에
액티비티에서 데이터를 추가하고 본 프래그먼트가 보이는 화면으로 돌아와도
데이터의 변화가 바로 나타나지는 않는다.
따라서 다음으로는 데이터 추가 후 새로고침 기능과 악보 검색기능을 추가할 계획이다.
다음은 새로고침이 되었을 때의 결과 화면이다.
다음으로는 리사이클러 뷰의 각각 카드뷰를 선택하면
그 안에서 구체적인 코드 악보를 볼 수 있도록 만들 것이다.
이를 위해서는 코드 악보를 테스트 용으로라도 넣을 수 있도록 입력 창을 만들어야 하고
입력 창을 만들기 전에 코드 악보를 저장할 수 있는 테이블을 DB에 만들어야하고
테이블을 DB에 만들기 위해 코드 악보를 저장하기 위한 테이블 레이아웃을 짜야한다..
이걸 다 하면 '악보' 탭의 핵심적인 기능 구현이 끝난다.
그리고 그룹과 마이페이지 탭의 기능 구현이 남아있다
'개인 프로젝트 > [2021] 코드악보 공유APP' 카테고리의 다른 글
7. 악보 검색 / 등록 페이지 제작 (5) - 악보 추가 기능 만들기(1) (2) | 2021.03.18 |
---|---|
6. 악보 검색 / 등록 페이지 제작 (4) - 검색기능 구현 & 악보 뷰어 제작 (0) | 2021.03.13 |
4. 악보 검색 / 등록 페이지 제작 (2) - 프래그먼트에 리사이클러 뷰 추가 (0) | 2021.03.11 |
3. 악보 검색 / 등록 페이지 제작 (1) - 바텀 네비게이션 추가 (0) | 2021.03.10 |
2. 안드로이드로 DB에 데이터를 저장하는 테스트 (2) | 2021.03.05 |