티스토리 뷰

input 태그를 이용하여 파일 업로드를 할 때, Chrome Browser에서는 잘 되지만,

앱 WebView에서는 동작을 하지 않는다.


결국 구글링과 Chrome 소스를 참고 해서 File Upload를 테스트 해 보았다.




내가 테스트한 WebView 셋팅 소스 이다.

가장 중요한 부분이 setWebChromeClient 함수 이다.


private static final String TYPE_IMAGE = "image/*";
private static final int INPUT_FILE_REQUEST_CODE = 1;

private ValueCallback<Uri> mUploadMessage;
private ValueCallback<Uri[]> mFilePathCallback;
private String mCameraPhotoPath;
WebView webView  = (WebView) findViewById(R.id.webview_);
webView.getSettings().setJavaScriptEnabled(true);

// Enable pinch to zoom without the zoom buttons
webView.getSettings().setBuiltInZoomControls(true);
// Enable pinch to zoom without the zoom buttons
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) {
// Hide the zoom controls for HONEYCOMB+
webView.getSettings().setDisplayZoomControls(false);
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH)
webView.getSettings().setTextZoom(100);
webView.setWebChromeClient(new WebChromeClient(){
@Override
public void onCloseWindow(WebView w) {
super.onCloseWindow(w);
finish();
}

@Override
public boolean onCreateWindow(WebView view, boolean dialog, boolean userGesture, Message resultMsg) {
final WebSettings settings = view.getSettings();
settings.setDomStorageEnabled(true);
settings.setJavaScriptEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
view.setWebChromeClient(this);
WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(view);
resultMsg.sendToTarget();
return false;
}

// For Android Version < 3.0
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
//System.out.println("WebViewActivity OS Version : " + Build.VERSION.SDK_INT + "\t openFC(VCU), n=1");
mUploadMessage = uploadMsg;
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(TYPE_IMAGE);
startActivityForResult(intent, INPUT_FILE_REQUEST_CODE);
}

// For 3.0 <= Android Version < 4.1
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
//System.out.println("WebViewActivity 3<A<4.1, OS Version : " + Build.VERSION.SDK_INT + "\t openFC(VCU,aT), n=2");
openFileChooser(uploadMsg, acceptType, "");
}

// For 4.1 <= Android Version < 5.0
public void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType, String capture) {
Log.d(getClass().getName(), "openFileChooser : "+acceptType+"/"+capture);
mUploadMessage = uploadFile;
imageChooser();
}

// For Android Version 5.0+
// Ref: https://github.com/GoogleChrome/chromium-webview-samples/blob/master/input-file-example/app/src/main/java/inputfilesample/android/chrome/google/com/inputfilesample/MainFragment.java
public boolean onShowFileChooser(WebView webView,
ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
System.out.println("WebViewActivity A>5, OS Version : " + Build.VERSION.SDK_INT + "\t onSFC(WV,VCUB,FCP), n=3");
if (mFilePathCallback != null) {
mFilePathCallback.onReceiveValue(null);
}
mFilePathCallback = filePathCallback;
imageChooser();
return true;
}

private void imageChooser() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
// Create the File where the photo should go
File photoFile = null;
try {
photoFile = createImageFile();
takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath);
} catch (IOException ex) {
// Error occurred while creating the File
Log.e(getClass().getName(), "Unable to create Image File", ex);
}

// Continue only if the File was successfully created
if (photoFile != null) {
mCameraPhotoPath = "file:"+photoFile.getAbsolutePath();
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
Uri.fromFile(photoFile));
} else {
takePictureIntent = null;
}
}

Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
contentSelectionIntent.setType(TYPE_IMAGE);

Intent[] intentArray;
if(takePictureIntent != null) {
intentArray = new Intent[]{takePictureIntent};
} else {
intentArray = new Intent[0];
}

Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
chooserIntent.putExtra(Intent.EXTRA_TITLE, "Image Chooser");
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);

startActivityForResult(chooserIntent, INPUT_FILE_REQUEST_CODE);
}
});
webView.setWebViewClient(new WebViewClient());


WebChromeClient 클래스에서 보내주는 콜백 함수를 아래와 같이 생각 하면 된다.


KitKat 버전 이하는 openFileChooser 함수가 콜백이 된다.

Lollipop 버전 이상은 onShowFileChooser 함수가 콜백이 된다.


이 때 imageChooser 함수는 카메라 기능과 갤러리에서 이미지를 가져 오는 기능을 한다.


takePictureIntent 는 카메라 기능

contentSelectionIntent 는 갤러리 기능

chooserIntent 는 위 두 기능을 사용자에게 묶어서 선택하도록 위한 기능


createImageFile은 참고 하시기 바란다. 이 함수는 원하는 대로 구현 하면 된다. 결국 파일 하나만 만들어서 리턴 해주면 끝이다.

/**
* More info this method can be found at
* http://developer.android.com/training/camera/photobasics.html
*
* @return
* @throws IOException
*/
private File createImageFile() throws IOException {
// Create an image file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES);
File imageFile = File.createTempFile(
imageFileName, /* prefix */
".jpg", /* suffix */
storageDir /* directory */
);
return imageFile;
}


이렇게 사용자가 선택한 사진 이미지는 아래 보이는 소스 같이 onActivityResult로 받아 온다.

여기서 WebPage 소스가 File 확장자가 이미지가 아니면 업로드하는 부분이 막혀 있어서,

KitKat 버전에서는 Android에서 제공해주는 Uri 대신 해당 File을 직접 찾아 File 경로에 대한 Uri를 넘겨 주었다.


여기서 테스트 할 때 느낀 부분은, filePath를 넘길 때 앞에 'file:' 이 문자열을 꼭 넣어줘야 한다.

그렇지 않으면 file 경로 인식이 잘 되지 않았다. 참고 하길 바란다.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == INPUT_FILE_REQUEST_CODE && resultCode == RESULT_OK) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (mFilePathCallback == null) {
super.onActivityResult(requestCode, resultCode, data);
return;
}
Uri[] results = new Uri[]{getResultUri(data)};

mFilePathCallback.onReceiveValue(results);
mFilePathCallback = null;
} else {
if (mUploadMessage == null) {
super.onActivityResult(requestCode, resultCode, data);
return;
}
Uri result = getResultUri(data);

Log.d(getClass().getName(), "openFileChooser : "+result);
mUploadMessage.onReceiveValue(result);
mUploadMessage = null;
}
} else {
if (mFilePathCallback != null) mFilePathCallback.onReceiveValue(null);
if (mUploadMessage != null) mUploadMessage.onReceiveValue(null);
mFilePathCallback = null;
mUploadMessage = null;
super.onActivityResult(requestCode, resultCode, data);
}
}

private Uri getResultUri(Intent data) {
Uri result = null;
if(data == null || TextUtils.isEmpty(data.getDataString())) {
// If there is not data, then we may have taken a photo
if(mCameraPhotoPath != null) {
result = Uri.parse(mCameraPhotoPath);
}
} else {
String filePath = "";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
filePath = data.getDataString();
} else {
filePath = "file:" + RealPathUtil.getRealPath(this, data.getData());
}
result = Uri.parse(filePath);
}

return result;
}

소스를 보시면 이해 하시겠지만, 카메라를 통한 경우에는 data 넘어오지 않고 mCameraPhotoPath의 데이터를 이용 한다.

갤러리는 data로 넘어오기 때문에 그 data를 이용하여 전달해 주면 된다.


getRealPath 소스는 아래 올려 놓은 소스를 이용하셔도 됩니다. Uri의 실제 경로를 넘겨주는 소스 입니다.

http://gogorchg.tistory.com/entry/Android-Get-RealPath-from-Uri


참고용으로 AndroidMenifest.xml에 권한 소스 이다.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />


이 소스를 가지고 4.4.4, 5.1.1, 6.0.2, 에서 무리 없이 동작이 잘 되는 것을 확인 했다.

혹시 문의 사항이 있으시면 댓글 달아 주길 바란다.


풀 소스:


WebViewTest.z01

WebViewTest.z02

WebViewTest.zip



참고 : 

        https://github.com/GoogleChrome/chromium-webview-samples/blob/master/input-file-example/app/src/main/java/inputfilesample/android/chrome/google/com/inputfilesample/MainFragment.java


        http://cofs.tistory.com/182

댓글
  • 이전 댓글 더보기
  • 프로필사진 관절분리 와.. 예제 돌리니까 바로 되네요 실제로 써봐야겠지만 감사합니다~ 2017.02.01 14:58 신고
  • 프로필사진 GsBOB 도움이 됐으면 좋겠네요~ 2017.02.01 16:06 신고
  • 프로필사진 비밀댓글입니다 2017.02.09 23:28
  • 프로필사진 GsBOB 오류가 어떻게 나시는지 알려주셔야 도움이 될듯 합니다.

    저두 그냥 대답을 해드리기가 어려워요 ㅎ
    2017.02.10 08:52 신고
  • 프로필사진 니버 혹시 메모장으로 받을수 있나요? 설명을 반복해서 읽고 있는데 어느부분에 어디에 넣어야 하는지 모르겠습니다.
    현재 구글링으로 웹뷰와 타이틀바 삭제 까지 성공한 상태입니다.
    정말 왕초보입니다. 컨닝으로 쫒아가고 있습니다.
    2017.02.10 21:21 신고
  • 프로필사진 비밀댓글입니다 2017.02.12 22:31
  • 프로필사진 GsBOB MainActivity를 어떻게 설정 되어 있는지 공유 받을 수 있을까요??

    비밀 댓글로 소스 넣어주시면 될듯 한데요.
    2017.02.13 08:45 신고
  • 프로필사진 유나아빠 샘플 소스를 실행해보면 카메라/갤러리 선택이 나오질 않고
    바로 갤러리로 이동합니다.
    원래 그런건가요?
    2017.02.22 09:53 신고
  • 프로필사진 GsBOB 넵. 샘플 소스에서는 chooserIntent 만 넘기게 되어 있어서 그렇네요^^

    intentArray 를 통해서 startActivties 로 실행 할수 있지만, 결과 값을 리턴 받기 위해선 이렇게 사용하지 않죠.

    카메라/ 갤러리 선택 하는 다이얼로그를 보이도록 만드시고 다이얼로그 콜백을 통해서 intent를 별도로 호출하시면 될듯 하네요 ㅎ
    2017.02.22 11:15 신고
  • 프로필사진 관절분리 그럼 갤러리말고 카메라만 사용하려면 수정이 많이 필요한가요? 2017.03.02 17:03 신고
  • 프로필사진 GsBOB 아닙니다. imageChooser 함수를 호출 하지 않고 Camera를 여는 Intent를 실행 하면 됩니다.

    Camera 호출 Intent는 아실꺼라 생각 됩니다. 구글링 하면 금방 나오니깐요^^
    2017.03.02 17:36 신고
  • 프로필사진 ㅋㅋ 안녕하세요 소스가 너무 좋아서 잘사용하려고합니다!
    지급 웹뷰위에 플로팅 버튼이 있는데 사진선택해서 등록해보니.. 플로팅 버튼까지 캡쳐가 된모습으로 이미지가 등록되더라구요..
    이게 정확히 어떻게 돌아가는건지 궁굼하네요ㅠ
    사진을 가지고와서 해주는거같은데.. 버튼이 왜나오는지..
    2017.03.14 13:22 신고
  • 프로필사진 GsBOB 죄송합니다만, 제가 댓글에 대한 이해를 잘 못 하겠네요. 하고자 하는 기능에 더 자세한 설명이 필요해보입니다.

    사진을 가져온다는 부분이 갤러리 에서 가져오는지 ui캡쳐를 말씀하시는지 모르겠네요.
    2017.03.14 13:43 신고
  • 프로필사진 ㅋㅋ 네 갤러리에서 가져와서 캡쳐를 하는건가요?
    아니면 갤러리에서 가져와서 바로 보내는건지.. 소스가 이해가 잘안되서요ㅠ
    2017.03.14 13:46 신고
  • 프로필사진 GsBOB 아닙니다. 그저 갤러리에서 가져온 이미지 파일의 URI을 넘겨주기만 하는거에요. 캡쳐 하는 기능은 되어 있지 않습니다. 2017.03.14 13:53 신고
  • 프로필사진 윤석 안녕하세요 ^^ 위의 소스를 이용하여 카메라 및 갤러리 업로드 잘 동작합니다 ^^/
    한가지 문제점이 생겼는데요.. 위의 소스를 사용할시 shouldOverrideUrlLoading이 제대로 동작하지를 않아서요 ㅎ
    소스는
    private class CustomWebViewClient extends WebViewClient {

    public static final String INTENT_PROTOCOL_START = "intent:";
    public static final String INTENT_PROTOCOL_INTENT = "#Intent;";
    public static final String INTENT_PROTOCOL_END = ";end;";
    public static final String GOOGLE_PLAY_STORE_PREFIX = "market://details?id=";

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
    /*
    * android.os.Build.VERSION.SDK_INT >= 19 안드로이드 4.4 이상인 경우
    */

    if (url.startsWith("tel:")) {

    Intent e = new Intent(Intent.ACTION_DIAL, Uri.parse(url));

    // ACTION_DIAL : 다이얼 , ACTION_CALL : 전화걸기

    startActivity(e);

    return true;

    }
    if (android.os.Build.VERSION.SDK_INT >= 19) {
    if (url.startsWith(INTENT_PROTOCOL_START)) {

    final int customUrlStartIndex = INTENT_PROTOCOL_START.length();
    final int customUrlEndIndex = url.indexOf(INTENT_PROTOCOL_INTENT);
    if (customUrlEndIndex < 0) {
    return false;
    } else {
    final String customUrl = url.substring(customUrlStartIndex, customUrlEndIndex);
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    try {
    intent.setData(Uri.parse(customUrl));
    getBaseContext().startActivity(intent);
    } catch (ActivityNotFoundException e) {
    final int packageStartIndex = customUrlEndIndex+ INTENT_PROTOCOL_INTENT.length();
    final int packageEndIndex = url.indexOf(INTENT_PROTOCOL_END);

    final String packageName = url.substring(packageStartIndex, packageEndIndex < 0 ? url.length() : packageEndIndex);
    intent.setData(Uri.parse(GOOGLE_PLAY_STORE_PREFIX + packageName));
    getBaseContext().startActivity( intent );
    }
    return true;
    }
    } else {
    return false;
    }
    } else {
    if (url.startsWith("intent:") || url.startsWith("kakaolink:") || url.startsWith("market:")) {
    Intent intent = new Intent(Intent.ACTION_VIEW,Uri.parse(url));
    startActivity(intent);
    } else {
    view.loadUrl(url);
    }
    return super.shouldOverrideUrlLoading(view, url);
    }
    }
    }

    카메라 및 갤러리 업로드 기능을 삭제하고 실행하면 shouldOverrideUrlLoading이 잘 작동해서요
    혹시 업로드 부분과 충돌? 같은게 일어나지 않나 생각되는데요 ㅠ_ㅠ
    실례가 안된다면 한번 봐주셨으면 좋겠습니다.

    그럼 환절기 건강 유의하세요~ ^^
    2017.03.15 17:53 신고
  • 프로필사진 GsBOB 에러 발생 로그는 없나요?? 에러 로그를 보면 좀 더 알 기 쉬울것 같아요.

    업로드 기능이 문제라고 하시니 왠지 Uri 전달 부분이 잘 되었는지 궁금하네요.
    2017.03.16 08:51 신고
  • 프로필사진 윤석 빌드시 아무런 에러 로그는 안나오네요 ㅠ_ㅠ 에러라도 나오면 저도 좀 찾아볼텐데요 ^^;;
    tel 스키마를 지정했음에도 불구하고 tel 링크를 클릭하면
    스키마가 지정되지 않았다는 오류 메시지가 나옵니다 ㅎㅎ
    답변 감사드리구요~ 더 찾아봐야겠네요.. 갤러리 소스는 잘쓰겠습니다.
    감사합니다. ^^
    2017.03.16 17:49 신고
  • 프로필사진 GsBOB 도움이 못 되어드려 죄송하네요. 잘 해결 되시길 바랄께요~ 2017.03.16 18:01 신고
  • 프로필사진 영재 안드로이드 킷켓 버전에서는 openFileChooser 함수가 호출이 안되는이슈가 있는데요 그부분은 어쩔수 없이 브릿지 연결해서 해결하는 방법뿐이 없겠죠? 2017.04.18 14:12 신고
  • 프로필사진 GsBOB 우선 위 소스의 주석 상으로는 openFileChooser가 불러져야 맞는데 안불러 진다면 브릿지가 맞겠죠... 2017.04.18 14:13 신고
  • 프로필사진 mistral 감사합니다. ^^ 잘 봤습니다. 한가지 궁금한게 있어서 여쭤봅니다. Image 파일 이외의 (.pdf) 파일은 업로드가 안되는지요? 2017.06.13 17:00 신고
  • 프로필사진 GsBOB 테스트 해보진 않았지만, input tag와 함께 사용 하는 거라 어떤 파일 형식이든 가능해 보입니다. 2017.06.13 18:14 신고
  • 프로필사진 SMK 감사합니다. 잘되는군요
    다만 파일을 한개가 이니라 여러개는 안되나요?
    html 에서 input 에 multiple를 추가해서 크롭이나 기타 브라우져에서는 여러개 선택이 되지만
    Webview에서는 여전히 한개만 선택할 수 있더라구요 혹시 여러개를 선택할 방법은 없을까요?
    2017.07.04 17:57 신고
  • 프로필사진 GsBOB http://www.masterqna.com/android/76018/%EC%9B%B9%EB%B7%B0%EC%97%90%EC%84%9C-%ED%8C%8C%EC%9D%BC-%EB%A9%80%ED%8B%B0-%EC%84%A0%ED%83%9D%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%98%EB%82%98%EC%9A%94

    https://stackoverflow.com/questions/36562849/multiple-file-upload-in-android-webview

    도움이 되실지 모르지만, 참고할만한 정보를 공유 드립니다.
    2017.07.05 09:20 신고
  • 프로필사진 JJ 안녕하세요.
    올려주신 소스보면서 처리중인데.. 혹시 한가지 궁금한게 4.4.2(킷캣) 의 경우에는 openFileChooser 함수 자체가 호출이 안되던데..
    혹시 4.4.2의 경우는 어떻게 처리 하셨는지 궁금하네요 ^^;
    2017.11.15 17:03 신고
  • 프로필사진 GsBOB 예외적으로 JavaScriptInterface 함수를 호출 하는 형태로 많이 하는 것을 보았습니다. 혹시 그외 방법이 있을까요??ㅎ 2017.11.15 18:13 신고
  • 프로필사진 JSJ 감사합니다! 잘 되네욥 ..
    한가지 문제점이 생겼는데요...
    처음 웹뷰에 접속을 했을때에는 파일선택을 누르면 잘 동작이 되는데
    뒤로 갔다가 다시 누를때에는 동작을 하지 않는데 왜 그런지 알 수 있을까요?
    2017.11.24 11:31 신고
  • 프로필사진 GsBOB 음.. 위 콜백 함수는 잘 호출 되나요?? 2017.11.24 12:01 신고
  • 프로필사진 킷캣은나의원수 음 4.4.4에서 파일또는 파일경로에 공백 또는 한글이 들어가면 정상적으로 올라가지 않는경우가있습니다
    확인결과 웹에서 파일정보를 urlencode가 된 상태로 들어지는 상태여서 그런거같네요 음..
    (이건 테스트앱에서도 같은증상이 났습니다. 5이상버전은 정상.)
    2018.01.10 10:12 신고
  • 프로필사진 GsBOB url encode 관련 문제도 있군요. ㅎ 좋은 정보 감사합니다. ~~ 2018.01.10 12:05 신고
  • 프로필사진 비밀댓글입니다 2018.01.12 16:14
  • 프로필사진 GsBOB http://gogorchg.tistory.com/entry/Android-Get-RealPath-from-Uri

    여기 참고하시면 됩니다^^
    2018.01.12 16:20 신고
  • 프로필사진 킷캣은나의원수 4.4.4 버전 한글과 띄어쓰기 이슈를 해결해볼려고 해봤습니다만 소용없군요 ㅋㅋㅋㅋ(테스트폰이 하나여서 지니모션으로도 테스트함 증상같음ㅋㅋㅋ)
    api19버전입니다만..

    아 미치겠네요 이거 어떻게 해결 못할까요 ㅋㅋㅋㅋㅋㅋ
    2018.01.16 15:54 신고
  • 프로필사진 GsBOB 꼭 한글로 업로드를 시켜야 하는 건가 보군요??
    업로드 시, 파일명을 바꾼 후 업로드 하는 것도 나쁘지 않아 보이는데요~
    2018.01.16 16:29 신고
  • 프로필사진 킷캣은나의원수 결국 해결봤습니다.
    http://acorn-world.tistory.com/62
    http://aroundck.tistory.com/3006

    두사이트를 참고해서 코드일부를 참조해서 해결보긴봤습니다.
    저와 같은 케이스의 분들을 위해 설명을 드리자면 위 사이트의 openfilechooser을 참고하시고 onactivityresult 에서 킷캣처리하는 부분은 아래사이트를 참고하시면됩니다.
    단, 제가찾은 방법은 어디까지나 임시방편입니다.
    댓글 일일이 달아주셔서 감사합니다 (ㅡ ㅡ) (_ _ )
    2018.01.16 18:36 신고
  • 프로필사진 킷캣은나의원수 일단 코드도 추가해서 올려드립니다.
    onactivityresult 부분에서 킷캣처리를 하실려면 이코드를 적용시키세요.
    (나머지는 냅두고 킷캣관련부분만 이걸로 고치시면됩니다. )

    if(requestCode == FILECHOOSER_RESULTCODE)
    {
    if(mUploadMessage == null)
    {
    return;
    }


    Uri result = null;

    if ( data != null || resultCode == RESULT_OK ) {

    result = data.getData();
    }

    mUploadMessage.onReceiveValue(result);


    }
    2018.01.16 18:40 신고
댓글쓰기 폼
공지사항
Total
204,346
Today
686
Yesterday
721
«   2018/01   »
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
글 보관함