펌 OK (출처 표시), 상업적 이용 NO, 컨텐츠 변경 NO



매니페스트에 설정된 어플의 버전을 가져오는 방법이다.


직접적으로 매니페스트의 버전을 가져올 수 없기때문에


패키지매니저를 통해 자기자신 패키지명을 대입하여


설치정보를 가져오는 방식이다.


public static String getPackageVersion(Context context) {

try {

PackageInfo pi = context.getPackageManager()

.getPackageInfo(context.getPackageName(),

PackageManager.GET_UNINSTALLED_PACKAGES);

return pi.versionName;

}

catch(Exception e) {

}

return new String("");

}


예를들어 매니페스트의 버전이 android:versionName="1.0.0" 이라면


1.0.0을 리턴한다.


버전체크가 필요할때 주로 사용되는 방법이다.



댓글을 달아 주세요

펌 OK (출처 표시), 상업적 이용 NO, 컨텐츠 변경 NO



웹뷰 내용을 롱클릭 하면 블럭설정이 된다.


기본적으로 롱클릭리스너에서 false를 리턴하기때문인데


아래와 같이 true로 수정하면 블럭지정이 되지 않는다.



mWebView.setOnLongClickListener(new OnLongClickListener() {

@Override

public boolean onLongClick(View v) {

return true;

}

});




댓글을 달아 주세요

Android/기본스킬 | Posted by 덩치 2014.11.17 17:19

전원버튼 이벤트 감지하기

펌 OK (출처 표시), 상업적 이용 NO, 컨텐츠 변경 NO


IntentFilter powerFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);

registerReceiver(mPowerBroadcast, powerFilter);



BroadcastReceiver mPowerBroadcast = new BroadcastReceiver() {

@Override

public void onReceive(Context context, Intent intent) {

if(Intent.getAction().equals("android.intent.action.SCREEN_OFF") {

// 스크린이 꺼질때 이벤트

}

else if(Intent.getAction().equals("android.intent.action.SCREEN_ON") {

// 스크린이 켜질때 이벤트

}

else

return;

}


}


그리고 onDestroy 등에서

unregisterReceiver(mPowerBroadcast);

를 이용해 브로드캐스트를 종료한다.



댓글을 달아 주세요

Android/기본스킬 | Posted by 덩치 2014.11.17 17:12

홈버튼 이벤트 감지하기

펌 OK (출처 표시), 상업적 이용 NO, 컨텐츠 변경 NO

@Override

protected void onUserLeaveHint() {

//여기서 감지

Log.d(TAG, "Home Button Touch");

super.onUserLeaveHint();

}


홈키 눌렀을때 작동되어야 하는 작업들을 위의 소스코드를 이용하여 처리할 수 있다.




댓글을 달아 주세요

  1.  댓글주소  수정/삭제  댓글쓰기 아데우스 2015.06.09 09:54

    100퍼센트 불가능입니다. 저 이벤트가 홈키때만 호출되는것이 아니라 여러 상황에 호출되기 때문에 예외처리를 추가해줘야해요 생명주기 순서를 따져서 홈키인지 아닌지를 판별해야합니다.

    •  댓글주소  수정/삭제 덩치 2015.06.10 18:10 신고

      좋은지적 감사합니다. 이 글을 참고하시는분들은 홈키를 판단하실때 생명주기가 홈키를 눌렀을때와 동일한 주기로 입력됐는지 확인하는 부분을 추가하시면 되겠습니다. 빠른 시일 내 업데이트 하겠습니다.

펌 OK (출처 표시), 상업적 이용 NO, 컨텐츠 변경 NO


LocationManager를 사용하기 위한 퍼미션 ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION 


모바일의 Gps 또는 Network 정보로 위치좌표를 받아오는 방법에 대해 알아보겠다.


LocationManager mLM = (LocationManager) getSystemService(Context.LOCATION_SERVICE);


이렇게 로케이션 매니저를 선언 한 뒤, 위치값 갱신을 호출 해 보자.


위치 제공자는 총 2가지 종류가 있다.


1. GPS_PROVIDER

2. NETWORK_PROVIDER


실내에서는 GPS_PROVIDER를 호출해도 응답이 없다. 응답을 기다리는 형태로 코딩을 했다면


별다른 처리를 하지 않으면 실내에서는 무한정 대기한다.


따라서 타이머를 설정하여 GPS_PROVIDER를 호출 한 뒤 일정 시간이 지나도 응답이 없을 경우


NETWORK_PROVIDER를 호출 하거나,


또는 둘 다 한꺼번에 호출하여 들어오는 값을 사용하는 방식으로 코딩을 하는것이 일반적이겠다.


private void registerLocationUpdates() {

        mLM.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,

1000, 1, mLocationListener);

mLM.requestLocationUpdates(LocationManager.GPS_PROVIDER,

1000, 1, mLocationListener);

//1000은 1초마다, 1은 1미터마다 해당 값을 갱신한다는 뜻으로, 딜레이마다 호출하기도 하지만

//위치값을 판별하여 일정 미터단위 움직임이 발생 했을 때에도 리스너를 호출 할 수 있다.

}



private final LocationListener mLocationListener = new LocationListener() {

public void onLocationChanged(Location location) {

//여기서 위치값이 갱신되면 이벤트가 발생한다.

//값은 Location 형태로 리턴되며 좌표 출력 방법은 다음과 같다.


    if (location.getProvider().equals(LocationManager.GPS_PROVIDER)) {

//Gps 위치제공자에 의한 위치변화. 오차범위가 좁다.

double longitude = location.getLongitude();    //경도

double latitude = location.getLatitude();         //위도

float accuracy = location.getAccuracy();        //신뢰도

    }

    else {

//Network 위치제공자에 의한 위치변화

//Network 위치는 Gps에 비해 정확도가 많이 떨어진다.

    }

   }

public void onProviderDisabled(String provider) {

}


public void onProviderEnabled(String provider) {

}


public void onStatusChanged(String provider, int status, Bundle extras) {

}

}


위와같이 사용하면 된다. 그리고 더이상 위치값을 호출하지 않아도 되는 경우에는

mLM.removeUpdates(mLocationListener);자원해제를 반드시 해 준다. 누락하면 딜레이마다 계속 호출한다.





댓글을 달아 주세요

  1.  댓글주소  수정/삭제  댓글쓰기 학생 2014.12.07 05:02

    굿굿굿굿 베리굿
    깔끔하고 핵심만 딱딱 집어낸 글 굿이에요!

  2.  댓글주소  수정/삭제  댓글쓰기 초보개발자 2017.10.01 20:38

    와 필요한부분만있어서 이해하기 좋네요 ㅎㅎ

  3.  댓글주소  수정/삭제  댓글쓰기 gunnoooow 2019.03.29 14:26 신고

    업무해결에 도움이 되었습니다 형님 ㅋㅋ

펌 OK (출처 표시), 상업적 이용 NO, 컨텐츠 변경 NO


현재 단말기의 전화번호를 가져오고자 할때 사용한다. 

     ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE)).getLine1Number()


그리고 아래는 단말기와 관련된 내용

 

  1. AndroidManifest.xml 에 아래 권한 추가
     <uses-permission android:name="android.permission.READ_PHONE_STATE" />

  2. 아래와 같이 Context.getSystemService 를 통해 TelephonyManager 를 가져옴.
       TelephonyManager telephony = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
        
  3. TelephonyManager 의 메소드 들 중, getLine1Number() 메소드가 전화번호를 반환
       String  telPhoneNo = telephony.getLine1Number();  

 

   출처 ( http://icess.egloos.com/3279459 )     





그리고 아래는 좀 더 많은 내용. 
분명 필요한 순간이 있을것이다. 잘 기억해두자.
=========================================================================================

android.telephony 패키지의 TelephonyManager클래스에서 담당한다.

단말기의 모뎀상태에 대한 정보를 얻기 위해서는 READ_PHONE_STATE권한이 필요하다.
AndroidManifest.xml파일에 아래 내용을 추가한다.
<uses-permission 
android:name="android.permission.READ_PHONE_STATE">
</uses-permission>

◆ 단말기의 모뎀상태 조회
TelephonyManager 객체를 얻기 위해서는 Context 객체에서 제공하는 getSystemService() 메서드를 이용한다.
TelephonyManager tm = (TelephonyManager)  
getSystemService(TELEPHONY_SERVICE);

음성통화 상태 조회
CALL_STATE_IDLE/CALL_STATE_OFFHOOK/CALL_STATE_RINGING 등의 값을 반환
Log.d("PHONE", "getCallState :" + tm.getCallState());
데이터통신 상태 조회
DATA_DISCONNECTED/DATA_CONNECTING/DATA_CONNECTED/DATA_SUSPENDED 등의 값을 반환
Log.d("PHONE", "getDataState :" + tm.getDataState());
단말기 ID 조회
GSM방식 IMEI 또는 CDMA방식의 MEID 값을 반환
Log.d("PHONE", "getDeviceId :" + tm.getDeviceId());
SW버전 조회
GSM방식의 IMEI/SV와 같은 SW버전을 반환
Log.d("PHONE", "getDeviceSoftwareVersion :" + tm.getDeviceSoftwareVersion());
전화번호 조회
GSM방식의 MSISDN과 같은 전화번호 반환
Log.d("PHONE", "getLine1Number :" + tm.getLine1Number());
국가코드 조회
현재 등록된 망 사업자의 MCC(Mobile Country Code)에 대한 ISO 국가코드 반환
Log.d("PHONE", "getNETWORKCountryIso :" + tm.getNetworkCountryIso());
Log.d("PHONE", "getSimCountryIso :" + tm.getSimCountryIso());
망 사업자코드 조회
현재 등록된 망 사업자의 MCC+MNC(Mobile Network Code) 반환
Log.d("PHONE", "getNetworkOperator :" + tm.getNetworkOperator());
Log.d("PHONE", "getSimOperator :" + tm.getSimOperator());
망 사업자명 조회
현재 등록된 망 사업자명 반환
Log.d("PHONE", "getNetworkOperatorName :" + tm.getNetworkOperatorName());
Log.d("PHONE", "getSimOperatorName :" + tm.getSimOperatorName());
망 시스템 방식 조회
현재 단말기에서 사용중인 망 시스템 방식을 반환
NETWORK_TYPE_UNKNOWN/
GSM방식 :  NETWORK_TYPE_GPRS/NETWORK_TYPE_EDGE/NETWORK_TYPE_UMTS/
NETWORK_TYPE_HSDPA/NETWORK_TYPE_HSUPA/NETWORK_TYPE_HSPA
CDMA방식 : NETWORK_TYPE_CDMA/NETWORK_TYPE_EVDO_0/NETWORK_TYPE_EVDO_A/NETWORK_TYPE_1xRTT
Log.d("PHONE", "getNetworkType :" + tm.getNetworkType());
단말기 종류 조회
단말기에서 지원하는 망의 종류를 반환
PHONE_TYPE_NONE/PHONE_TYPE_GSM/PHONE_TYPE_CDMA 등의 값을 반환
Log.d("PHONE", "getPhoneType :" + tm.getPhoneType());
SIM카드 Serial Number 조회
Log.d("PHONE", "getSimSerialNumber :" + tm.getSimSerialNumber());
SIM카드 상태 조회
SIM_STATE_UNKNOWN/SIM_STATE_ABSENT/SIM_STATE_PIN_REQUIRED/SIM_STATE_PUK_REQUIRED/
SIM_STATE_NETWORK_LOCKED/SIM_STATE_READY 등의 값을 반환
Log.d("PHONE", "getSimState :" + tm.getSimState());
가입자 ID 조회
GSM방식의 IMSI와 같은 가입자 ID 반환
Log.d("PHONE", "getSubscriberId :" + tm.getSubscriberId());

◆ 조회결과
getCallState :0(CALL_STATE_IDLE)
getDataState :2(DATA_ACTIVITY_OUT)
getDeviceId :000000000000000
getDeviceSoftwareVersion :null
getLine1Number :15555218135
getNetworkCountryIso :us
getNetworkOperator :310260
getNetworkOperatorName :Android
getNetworkType :3(NETWORK_TYPE_UMTS)
getPhoneType :1(PHONE_TYPE_GSM)
getSimCountryIso :us
getSimOperator :310260
getSimOperatorName :Android
getSimSerialNumber :89014103211118510720
getSimState :5(SIM_STATE_READY)
getSubscriberId :310260000000000

실제상황에서는 이와같이 현재 단말기의 상태를 직접 조회하는것이 아니라 
상태정보가 변경될 때 자동으로 인식해야하는 상황이 훨씬 많을것이다.
이럴때는 TelephonyManager의 listen()를 이용하여 콜백메서드를 등록하면 가능하다.
        tm.listen(new PhoneStateListener(){
         public void onCallStateChanged(int state, String incomingNumber){
         if (state == TelephonyManager.CALL_STATE_RINGING){
         Log.d("Telephony", "state = " + state + ", number = " + incomingNumber);
         }else{
         Log.d("Telephony", "state = " + state);
         }        
         }
        }, PhoneStateListener.LISTEN_CALL_STATE);

onCallStateChanged() 이외에도 아래와 같은 상태변화를 감지할 수 있다.
- onCallForwardingIndicatorChanged() : 호전환(Call Forwarding) 상태 변화
- onCellLocationChanged() : 단말기의 Cell위치 변화
- onDataActivity() : Data 활성화 상태 변화
- onDataConnectionStateChanged() : Data 연결상태 변화
- onMessageWaitingIndicatorChanged() : 메시지 대기상태 변화
- onServiceStateChanged() : 단말기의 서비스 상태 변화
- onSignalStrengthsChanged() : 망의 신호강도 변화

◆ 전화번호 처리
각 국가별로 전화번호의 형식이 다르다. 
한국은 01x-xxxx-xxxx 이 일반적이고,
북미는 xxx-xxx-xxxx가 된다.
PhoneNumberUtils.formatNumber() 메서드를 사용하면 설정된 Locale에 맞게 형식화 된다.
String formattedTelNumber = PhoneNumberUtils.formatNumber("01098761234");
        Log.d("Telephony", "formattedTelNumber :" + formattedTelNumber);
Locale을 English(United States)로 할경우 010-987-61234로 출력된다.
한국어로 할 경우는 01098761234가 그대로 출력된다.
formatNumber() 메서드의 구현내용을 살펴봐야 할듯 하다.

EditText에서 전화번호를 입력받을 경우에도 자동으로 형식화가 가능하다.
EditText객체의 addTextChangedListener에 PhoneNumberFormattingTextWatcher()를 등록하기만 하면 된다.
        EditText tn = (EditText) findViewById(R.id.edtTelNumber);
        tn.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
그러나 역시 한국어로는 동작하지 않았다


댓글을 달아 주세요

펌 OK (출처 표시), 상업적 이용 NO, 컨텐츠 변경 NO




웹뷰(Javascript)와 안드로이드간 메소드를 호출하려고 하는데 ,


안드로이드 > 자바스크립트로는 메소드 호출이 되지만


자바스크립트 > 안드로이드로는 메소드 호출이 안되는 경우가 있다.



오늘 이 현상때문에 개고생을 하다가 결국 원인을 찾아냈다.



수없이 올라오는 로그를 자세히 보니 웹뷰에서 안드로이드의 메소드를 호출 할 때


Writing exception to parcel

java.lang.SecurityException: Permission Denial: get/set setting for user asks to run as user -2 but is calling from user 0; this requires android.permission.INTERACT_ACROSS_USERS_FULL

at com.android.server.am.ActivityManagerService.handleIncomingUser(ActivityManagerService.java:13192)

at android.app.ActivityManager.handleIncomingUser(ActivityManager.java:2044)

at com.android.providers.settings.SettingsProvider.callFromPackage(SettingsProvider.java:615)

at android.content.ContentProvider$Transport.call(ContentProvider.java:279)

at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:273)

at android.os.Binder.execTransact(Binder.java:388)

at dalvik.system.NativeStart.run(Native Method)



이런 에러코드가 발생한다는걸 알았고,


소스코드를 아무리 변경해봐도 안되고, 퍼미션에 찾아봐도 저런 퍼미션은 없었다.


원인은 AndroidManifest.xml 파일의 android:targetSdkVersion="18" 이 문제였다.


웹뷰 > 안드로이드는 4.2버전부터 호출이 막혔다고 한다.


(http://ttorr.blogspot.kr/2014/03/kitkat44-loadurl.html)



android:targetSdkVersion16 이하로 설정하거나, 아예 없어버린 뒤 해결되었다.


==================================================================================


14/10/14 추가 : import android.webkit.JavascriptInterface; + 메소드에 @JavascriptInterface 어노테이션을 추가하면 api level 17 이상에서도 작동한다.

(http://stackoverflow.com/questions/16353430/appview-addjavascriptinterface-does-not-work-on-api-17)


댓글을 달아 주세요

  1.  댓글주소  수정/삭제  댓글쓰기 1 2016.11.27 03:07

    마지막 방법은 추가해도 계속 자동적으로 사라지고
    첫번째 방법을 해도 실행이 되지않습니다..

펌 OK (출처 표시), 상업적 이용 NO, 컨텐츠 변경 NO





Handlermsgint값을 보낼 수 있다는것은 알것이다.

(http://biig.tistory.com/34  -  핸들러 사용법 포스팅)



그렇다면, 메시지로 객체를 전달할 수는 없을까 ??


할 수 있다.


기존에는


handler.sendEmptyMessage(int);


형식으로 메시지를 전송했다면,



같은 위치에서


Message msg = handler.obtainMessage();


으로 선언 해 주면


msg.what(int);

msg.obj(Object);

msg.arg1(int);

msg.arg2(int);


이런식으로 메시지에 객체를 담을 수 도 있고, 인트값도 저렇게 넣어 줄 수 있다.


잘 활용한다면 매우 유용하다.


예를들어 다른 액티비티에 객체를 전달해야 하는 경우, parcelable를 상속받고 기타 과정이 복잡한데


Handler를 통해 Object를 넘겨주면 매우 간단하게 해결이 가능하다.


그리고 sendEmptyMessage 대신


handler.sendMessage(msg);


로 해주면 전달이 완료된다.




댓글을 달아 주세요

Android/기본스킬 | Posted by 덩치 2014.01.27 15:28

기본적인 뷰 조작

펌 OK (출처 표시), 상업적 이용 NO, 컨텐츠 변경 NO


Study_exam_01.zip


예제파일 import해서 실행시켜보시기 바랍니다.

해석은 주석에 다 달려 있으니, 응용하여 연습하시면 더 좋으리라 생각됩니다.




package com.example.study_exam_01;


import com.example.study_1st.R;


import android.app.Activity;

import android.graphics.Color;

import android.os.Bundle;

import android.util.TypedValue;

import android.view.View;

import android.view.View.OnClickListener;

import android.widget.Button;

import android.widget.TextView;


public class MainActivity extends Activity implements OnClickListener {

private Button text_edit_bt;

private Button color_edit_bt;

private Button size_edit_bt;

private TextView tv1;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main); //이 액티비티는 activity_main.xml 파일의 뷰를 가집니다.

tv1 = (TextView) findViewById(R.id.tv1); //xml파일에서 설정한 뷰를 소스코드에서 사용하기 위해 ID로 연결하는 과정입니다.

text_edit_bt = (Button) findViewById(R.id.text_edit_bt);

color_edit_bt = (Button) findViewById(R.id.color_edit_bt);

size_edit_bt = (Button) findViewById(R.id.size_edit_bt);

text_edit_bt.setOnClickListener(this);

color_edit_bt.setOnClickListener(this);

size_edit_bt.setOnClickListener(this);

// text_edit_bt.setOnClickListener(new View.OnClickListener() {  //이렇게 각각의 리스너를 등록해서 사용 할 수도 있습니다.

// @Override

// public void onClick(View v) {

// tv1.setText(tv1.getText() + "+");

// }

// });

}


//온클릭을 오버라이드 하기 위해서는 클래스에 OnClickListener를 implements해야합니다. 11번째줄 참조.

@Override

public void onClick(View v) {

switch (v.getId()) {

case R.id.text_edit_bt :

tv1.setText(tv1.getText() + "+");  //기존 텍스트를 얻어와서(tv1.getText()) 뒤에 +를 추가합니다.

// tv1.setText("원하는 텍스트");

break;

case R.id.color_edit_bt :

tv1.setTextColor(Color.BLUE);  //Color 클래스에 들어있는 BLUE 색상으로 변환합니다.

// tv1.setTextColor(Color.parseColor("#FF0000")); //직접 색상코드를 입력하여 원하는 색으로도 설정 가능합니다.

break;

case R.id.size_edit_bt :

tv1.setTextSize(TypedValue.COMPLEX_UNIT_PX, tv1.getTextSize() + 1); //기존 텍스트사이즈를 얻어와서 + 1만큼 사이즈를 키웁니다.

// tv1.setTextSize(16); //원하는 고정수치(px)로도 텍스트 크기를 변경 가능합니다.

break;

}

}

}








<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <TextView
        android:id="@+id/tv1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />
    <Button
        android:id="@+id/text_edit_bt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="글 변경" />
    
    <Button
        android:id="@+id/color_edit_bt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="글씨 색 변경" />
    
    <Button
        android:id="@+id/size_edit_bt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="글씨 크기 변경" />
    
</LinearLayout>


댓글을 달아 주세요

Android/기본스킬 | Posted by 덩치 2014.01.27 14:46

안드로이드 생명주기

펌 OK (출처 표시), 상업적 이용 NO, 컨텐츠 변경 NO


안드로이드도 다른 프로세스와 마찬가지로 태어나고 죽기까지의 생명주기(life cycle)가 존재합니다.

위 다이어그램은 액티비티가 생성되고 죽기까지의 과정을 순서적으로 나타낸것입니다.


우선 액티비티에 대해 설명하자면, 액티비티는 단말 화면상으로 보이는 뷰(View) 즉 화면을 뜻합니다.

액티비티 위에는 다른 뷰들이 올라가게 되고, 그런 뷰들로 화면이 구성됩니다. 즉 액티비티는 도화지, 뷰(버튼,텍스트 등)는

그림이라고 생각하시면 이해하기 편합니다.


위 그림을 해석해 보면


onCreate - 액티비티가 시작되면 제일 먼저 onCreate()가 호출됩니다.

이 영역에서 어플이 켜짐과 동시에 실행되어야 하는 작업들을 실행하게 됩니다.

어플 최초 실행시의 로딩화면 등을 예로 들 수 있겠습니다.


onStart() - onCreate가 호출 된 후 바로 실행됩니다. 

(onCreate에 의해 실행되는것은 아닙니다. 모든 생명주기는 독립적입니다.)


onPause() - 다른 액티비티가 기존 액티비티 위에 생성되어 포커스를 잃은상태. 

반투명 또는 일부영역만 차지하는 액티비티가 호출 된 상태로, 액티비티의 일부가 화면상에 노출되고있는 상태입니다.


onReasure() - Pause상태에서 다시 액티비티가 활성화되면 호출됩니다.


onStop() - 액티비티가 가려지거나 숨겨졌을 때 호출됩니다. 

일반적으로 홈키를 눌렀을때 어플의 상태를 생각하시면 됩니다.


onRestart() - stop상태에서 다시 액티비티가 실행되면 호출됩니다.


onDeastroy() - 메모리상에서 액티비티의 자원이 완전 해제될 때 호출됩니다.

즉 어플을 종료할 때 사용됩니다. 어플 종료시 자원의 해제와 같은 기능을 여기에서 주로 실행하게 됩니다.

댓글을 달아 주세요